Skip to content

Commit

Permalink
support add / modify / remove entities
Browse files Browse the repository at this point in the history
  • Loading branch information
larry-wong committed Feb 26, 2023
1 parent 85fe311 commit 42642cf
Show file tree
Hide file tree
Showing 9 changed files with 287 additions and 111 deletions.
12 changes: 1 addition & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,17 +35,7 @@

A: 巴法云中灯的颜色和色温为同一个字段,此插件中无法精确区分。如果你的灯既可以调节颜色又可以调节色温,可能会出现混乱的情况。

- Q: 如何更改实体名字?

A: 默认名字为此实体在 Home Assistant 平台上的名字,因此可以修改 Home Assistant 平台上的名字,然后重新配置插件;

或者,可以直接登陆巴法云平台修改主题名,不过此方法会在下次重新配置插件时同步回去。

- Q: 配置完成之后如何增加/删除实体?

A: 可再次点击集成左下角的“选项”,重新选择需要同步的实体,重新选择时会默认选中上次配置的实体。

- Q: Home Assistant 中米家的设备本来就支持小爱同学配置,还需要同步至巴法云么?
- Q: Home Assistant 中米家的设备本就支持小爱同学配置,还需同步至巴法云么?

A: 不需要,并且不建议同步。因为受巴法云平台限制,某些指令并不支持。例如空调类设备不支持“空调制冷”,“空调制热”这样的指令,只支持“空调加热”。

Expand Down
2 changes: 1 addition & 1 deletion custom_components/bemfa/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant

from .const import CONF_INCLUDE_ENTITIES, CONF_UID, DOMAIN
from .const import CONF_UID, DOMAIN
from .mqtt import BemfaMqtt
from .service import BemfaService

Expand Down
186 changes: 136 additions & 50 deletions custom_components/bemfa/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,14 @@
from homeassistant.data_entry_flow import FlowResult
import homeassistant.helpers.config_validation as cv

from .const import CONF_INCLUDE_ENTITIES, CONF_UID, DOMAIN
from .const import (
CONF_UID,
DOMAIN,
OPTIONS_NAME,
OPTIONS_OPERATION,
OPTIONS_SELECT,
Operation,
)
from .service import BemfaService

_LOGGER = logging.getLogger(__name__)
Expand Down Expand Up @@ -73,83 +80,162 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
"""Handle a option flow for bemfa."""

_config_entry: config_entries.ConfigEntry
_topic_map: dict[str, tuple[str, str | None]]
_user_input: dict[str, str] = {}

# a dict to hold id/topic_2_name mapping when add / modify a sync
# with this map we can get default name in next step
_name_dict: dict[str, str] = {}

def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
"""Initialize options flow."""
self._config_entry = config_entry

# We guide one to sellect entities he wants to sync to bemfa service.
# Then we make http calls to submit his selection.
# When reconfiguring, entities selected last time will be checked by default.
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Manage the options."""
return self.async_show_menu(
step_id="init",
menu_options=["add_sync", "modify_sync", "remove_sync"],
)

async def async_step_add_sync(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Sync a hass entity to bemfa service"""
if user_input is not None:
return await self.async_step_set_sync_name(user_input)

service = self._get_service()
all_entities = service.get_supported_entities()
topic_map = await service.fetch_synced_data_from_server()
synced_entity_ids = set(
[
item[1]
for item in filter(lambda item: item[1] is not None, topic_map.values())
]
)

# filter out unsynced entities
self._name_dict.clear()
for entity in filter(
lambda entity: entity.entity_id not in synced_entity_ids, all_entities
):
self._name_dict[entity.entity_id] = entity.name

# select entities
entities = service.get_supported_entities()
self._topic_map = await service.fetch_synced_data_from_server()
self._user_input[OPTIONS_OPERATION] = Operation.ADD

return self.async_show_form(
step_id="entities",
step_id="add_sync",
data_schema=vol.Schema(
{
vol.Required(
CONF_INCLUDE_ENTITIES,
default=[
item[1]
for item in filter(
lambda item: item[1] is not None,
self._topic_map.values(),
)
],
): cv.multi_select(
{entity.entity_id: entity.name for entity in entities}
),
vol.Required(OPTIONS_SELECT): vol.In(
{
entity_id: name + " (" + entity_id + ")"
for (entity_id, name) in self._name_dict.items()
}
)
}
),
last_step=True,
last_step=False,
)

async def async_step_entities(
async def async_step_modify_sync(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Sync selected entities to bemfa service."""
"""Modify a hass-to-bemfa sync"""
if user_input is not None:
return await self.async_step_set_sync_name(user_input)

service = self._get_service()
topic_map = await service.fetch_synced_data_from_server()

current_entities: dict(str, tuple(str, str)) = {}
for (topic, (name, entity_id)) in self._topic_map.items():
if entity_id is None:
# remove unused topics
await service.remove_topic(topic)
else:
current_entities[entity_id] = (topic, name)
# filter out synced entities
self._name_dict.clear()
topic_id_dict: dict[str, str] = {}
for (topic, [name, entity_id]) in topic_map.items():
if entity_id is not None:
self._name_dict[topic] = name
topic_id_dict[topic] = entity_id

current_entity_ids = set(current_entities.keys())
new_entity_ids = set(user_input[CONF_INCLUDE_ENTITIES])
self._user_input[OPTIONS_OPERATION] = Operation.MODIFY

# removed entites
for entity_id in current_entity_ids - new_entity_ids:
await service.remove_topic(current_entities[entity_id][0])
return self.async_show_form(
step_id="modify_sync",
data_schema=vol.Schema(
{
vol.Required(OPTIONS_SELECT): vol.In(
{
topic: name + " (" + topic_id_dict[topic] + ")"
for (topic, name) in self._name_dict.items()
}
)
}
),
last_step=False,
)

# renamed entities?
for entity_id in current_entity_ids & new_entity_ids:
state = self.hass.states.get(entity_id)
if state is None:
continue
if state.name != current_entities[entity_id][1]:
await service.rename_topic(current_entities[entity_id][0], state.name)
async def async_step_remove_sync(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Remove hass-to-bemfa sync(s)"""
if user_input is not None:
service = self._get_service()
for topic in user_input[OPTIONS_SELECT]:
await service.remove_topic(topic)
self._user_input.clear()
return self.async_create_entry(title="", data=None)

# added entities
for entity_id in new_entity_ids - current_entity_ids:
await service.add_topic(entity_id)
# end to sync
service = self._get_service()
topic_map = await service.fetch_synced_data_from_server()

return self.async_create_entry(
title="",
data=user_input,
return self.async_show_form(
step_id="remove_sync",
data_schema=vol.Schema(
{
vol.Required(OPTIONS_SELECT): cv.multi_select(
{
topic: name
+ (" (" + entity_id + ")" if entity_id is not None else "")
for (topic, [name, entity_id]) in topic_map.items()
}
)
}
),
last_step=False,
)

async def async_step_set_sync_name(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Set details of a sync"""
self._user_input.update(user_input)
if OPTIONS_NAME in self._user_input:
service = self._get_service()
if self._user_input[OPTIONS_OPERATION] == Operation.ADD:
await service.add_topic(
self._user_input[OPTIONS_SELECT],
self._user_input[OPTIONS_NAME],
)
else:
await service.rename_topic(
self._user_input[OPTIONS_SELECT],
self._user_input[OPTIONS_NAME],
)
self._user_input.clear()
self._name_dict.clear()
return self.async_create_entry(title="", data=None)

return self.async_show_form(
step_id="set_sync_name",
data_schema=vol.Schema(
{
vol.Required(
OPTIONS_NAME,
default=self._name_dict[self._user_input[OPTIONS_SELECT]],
): str
}
),
)

def _get_service(self) -> BemfaService:
Expand Down
15 changes: 14 additions & 1 deletion custom_components/bemfa/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,24 @@

from typing import Final

from homeassistant.backports.enum import StrEnum

DOMAIN: Final = "bemfa"

# #### Config ####
CONF_UID: Final = "uid"
CONF_INCLUDE_ENTITIES: Final = "include_entities"

# ### Options ###
class Operation(StrEnum):
"""Operation for bemfa integration"""

ADD = "add"
MODIFY = "modify"


OPTIONS_OPERATION: Final = "operation"
OPTIONS_SELECT: Final = "select"
OPTIONS_NAME: Final = "name"

# #### MQTT ####
MQTT_HOST: Final = "bemfa.com"
Expand Down
18 changes: 9 additions & 9 deletions custom_components/bemfa/manifest.json
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
{
"domain": "bemfa",
"name": "Bemfa",
"version": "1.1.1",
"codeowners": [
"@larry-wong"
],
"config_flow": true,
"dependencies": [],
"documentation": "https://github.com/larry-wong/bemfa",
"homekit": {},
"iot_class": "cloud_push",
"issue_tracker": "https://github.com/larry-wong/bemfa/issues",
"requirements": ["paho-mqtt==1.6.1"],
"ssdp": [],
"zeroconf": [],
"homekit": {},
"dependencies": [],
"codeowners": [
"@larry-wong"
],
"iot_class": "cloud_push"
}
"version": "1.2.0",
"zeroconf": []
}
6 changes: 3 additions & 3 deletions custom_components/bemfa/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ def _start(event: Event | None = None):

async def fetch_synced_data_from_server(
self,
) -> dict[str, tuple[str, str | None]]: # topic -> [name, entity_id]
) -> dict[str, tuple[str, str | None]]: # topic -> [name, entity_id | None]
"""Fetch data we've synchronized to bemfa service."""
all_topics = await self._bemfa_http.async_fetch_all_topics()

Expand Down Expand Up @@ -92,13 +92,13 @@ def get_supported_entities(self) -> list[State]:
)
return entities

async def add_topic(self, entity_id: str):
async def add_topic(self, entity_id: str, name: str):
"""Sync an topic to bemfa service"""
state = self._hass.states.get(entity_id)
if state is None:
return
topic = generate_topic(state.domain, entity_id)
await self._bemfa_http.async_add_topic(topic, state.name)
await self._bemfa_http.async_add_topic(topic, name)
self._bemfa_mqtt.add_topic(topic, entity_id)

async def rename_topic(self, topic: str, name: str):
Expand Down
Loading

0 comments on commit 42642cf

Please sign in to comment.