Skip to content

Commit

Permalink
pack sensors of an area to single bemfa device
Browse files Browse the repository at this point in the history
  • Loading branch information
larry-wong committed Mar 31, 2023
1 parent 42642cf commit f5b8e1a
Show file tree
Hide file tree
Showing 22 changed files with 1,430 additions and 795 deletions.
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@
[![hacs_badge](https://img.shields.io/badge/HACS-Default-41BDF5.svg?style=for-the-badge)](https://github.com/hacs/integration)

## 使用
1. 注册巴法云账号,并获取 uid
1. 注册巴法云账号,并获取密钥
2. 在HACS中搜索 bemfa 安装,或者 clone 此项目, 将 custom\_components/bemfa 目录拷贝至 Home Assistant 配置目录的 custom\_components 目录下。
3. 重启 Home Assistant 服务。
4. 在 Home Assistant 的集成页面,搜索 "bemfa" 并添加。
5. 根据提示输入巴法云 uid 后提交
5. 根据提示输入巴法云密钥后提交
6. 安装成功后,点击集成左下角“选项”,同步需要的实体至巴法云。
7. 在智能音箱App中添加巴法云设备:
* 小爱同学: 在米家app-->我的-->其他平台设备-->点击添加-->找到"巴法",输入巴法云账号即可,设备会自动同步到米家。
Expand All @@ -29,21 +29,21 @@
## Q/A
- Q: 哪些实体支持同步至巴法云?

A: 受巴法云的限制,目前仅支持开关类,灯类,风扇类,窗帘类,空调类和温度/湿度/开关/光照传感器,并且对每种语音助手的支持各有稍许区别,例如小度音箱不支持风扇类和空调类实体,具体参考[巴法云文档](https://cloud.bemfa.com/docs/#/)。此外,此插件将扫地机/脚本/自动化/场景/二元选择器/分组/摄像机/加湿器/媒体播放器/锁/遥控器虚拟成开关类设备,可通过语音开关。
A: 受巴法云的限制,目前仅支持开关类,灯类,风扇类,窗帘类,空调类和温度/湿度/开关/光照传感器,并且对每种语音助手的支持各有稍许区别,例如小度音箱不支持风扇的摇头控制,具体参考[巴法云文档](https://cloud.bemfa.com/docs/#/)。此外,此插件将扫地机/脚本/自动化/场景/二元选择器/分组/摄像机/加湿器/媒体播放器/锁/遥控器/汽笛虚拟成开关类设备,可通过语音开关。

- Q: 为什么调节灯的颜色时却是调的色温?

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

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

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

- Q: 同时有小爱同学和天猫精灵,如何只同步非米家设备至小爱同学,并同步所有设备至天猫精灵?

A: 目前没有太好的方案,一个可行的方案是注册2个巴法云账号,分别配置不同的实体进行同步,然后将2个账号分别绑定到小爱同学和天猫精灵。
A: 目前没有太好的方案,一个可行的方案是注册2个巴法云账号,分别配置不同的插件实体进行同步,然后将2个账号分别绑定到小爱同学和天猫精灵。

## 捐赠
如果此项目对你有帮助,可以考虑请我喝杯咖啡 :)
如果此项目对你有帮助,可以扫描下方二维码请我喝杯咖啡 :)

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <img src="donate/wechat.png" width="200" title="使用微信扫一扫" /> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <img src="donate/alipay.png" width="200" title="使用支付宝扫一扫" />
18 changes: 15 additions & 3 deletions custom_components/bemfa/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,31 @@
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant

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

from . import (
sync_binary_sensor,
sync_sensor,
sync_light,
sync_fan,
sync_cover,
sync_climate,
sync_switch,
)

_LOGGING = logging.getLogger(__name__)


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up bemfa from a config entry."""
hass.data.setdefault(DOMAIN, {})

service = BemfaService(hass, entry.data.get(CONF_UID))
await service.start()
service = BemfaService(hass, entry.data[CONF_UID])
await service.async_start(
entry.options[OPTIONS_CONFIG] if OPTIONS_CONFIG in entry.options else {}
)

hass.data[DOMAIN][entry.entry_id] = {
"service": service,
Expand Down
236 changes: 138 additions & 98 deletions custom_components/bemfa/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,19 @@
import logging
import re
from typing import Any

import voluptuous as vol

from homeassistant import config_entries
from homeassistant.core import callback
from homeassistant.data_entry_flow import FlowResult
import homeassistant.helpers.config_validation as cv

from .sync import SYNC_TYPES, Sync
from .const import (
CONF_UID,
DOMAIN,
OPTIONS_NAME,
OPTIONS_OPERATION,
OPTIONS_CONFIG,
OPTIONS_SELECT,
Operation,
)
from .service import BemfaService

Expand Down Expand Up @@ -79,60 +77,64 @@ def async_get_options_flow(
class OptionsFlowHandler(config_entries.OptionsFlow):
"""Handle a option flow for bemfa."""

_config_entry: config_entries.ConfigEntry
_user_input: dict[str, str] = {}
# creat or modify a sync
_is_create: bool

# a dict to hold syncs when create / modify one of them
# with this map we can get it in the next step
_sync_dict: dict[str, Sync]

# 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] = {}
# current sync we are creating or modifu
_sync: Sync

def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
"""Initialize options flow."""
self._config_entry = config_entry
self._entry_id = config_entry.entry_id
self._config = (
config_entry.options[OPTIONS_CONFIG].copy()
if OPTIONS_CONFIG in config_entry.options
else {}
)

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"],
menu_options=[
"create_sync",
"modify_sync",
"destroy_sync",
],
)

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

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())
]
)
all_topics = await service.async_fetch_all_topics()
all_syncs = service.collect_supported_syncs()
self._sync_dict = {}
for sync in all_syncs:
if sync.topic not in all_topics:
self._sync_dict[sync.entity_id] = sync

# 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

self._user_input[OPTIONS_OPERATION] = Operation.ADD
self._is_create = True

return self.async_show_form(
step_id="add_sync",
step_id="create_sync",
data_schema=vol.Schema(
{
vol.Required(OPTIONS_SELECT): vol.In(
{
entity_id: name + " (" + entity_id + ")"
for (entity_id, name) in self._name_dict.items()
sync.entity_id: sync.generate_option_label()
for sync in self._sync_dict.values()
}
)
}
Expand All @@ -143,100 +145,138 @@ async def async_step_add_sync(
async def async_step_modify_sync(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Modify a hass-to-bemfa sync"""
"""Modify a hass-to-bemfa sync."""
if user_input is not None:
return await self.async_step_set_sync_name(user_input)
self._sync = self._sync_dict[user_input[OPTIONS_SELECT]]
return await self._async_step_sync_config()

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

# 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
all_topics = await service.async_fetch_all_topics()
all_syncs = service.collect_supported_syncs()
self._sync_dict = {}
for sync in all_syncs:
if sync.topic in all_topics:
sync.name = all_topics[sync.topic]
self._sync_dict[sync.entity_id] = sync

self._user_input[OPTIONS_OPERATION] = Operation.MODIFY
self._is_create = False

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()
sync.entity_id: sync.generate_option_label()
for sync in self._sync_dict.values()
}
)
}
),
last_step=False,
)

async def async_step_remove_sync(
async def _async_step_sync_config(self) -> FlowResult:
"""Set details of a hass-to-bemfa sync."""
if self._sync.topic in self._config:
self._sync.config = self._config[self._sync.topic]

return self.async_show_form(
step_id=self._sync.get_config_step_id(),
data_schema=vol.Schema(self._sync.generate_details_schema()),
)

async def async_step_sync_config_sensor(
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)
"""Set details of a hass-to-bemfa sensor sync."""
return await self._async_step_sync_config_done(user_input)

service = self._get_service()
topic_map = await service.fetch_synced_data_from_server()
async def async_step_sync_config_binary_sensor(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Set details of a hass-to-bemfa binary sensor sync."""
return await self._async_step_sync_config_done(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_sync_config_climate(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Set details of a hass-to-bemfa climate sync."""
return await self._async_step_sync_config_done(user_input)

async def async_step_sync_config_cover(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Set details of a hass-to-bemfa cover sync."""
return await self._async_step_sync_config_done(user_input)

async def async_step_sync_config_fan(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Set details of a hass-to-bemfa fan sync."""
return await self._async_step_sync_config_done(user_input)

async def async_step_sync_config_light(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Set details of a hass-to-bemfa light sync."""
return await self._async_step_sync_config_done(user_input)

async def async_step_sync_config_switch(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Set details of a hass-to-bemfa switch sync."""
return await self._async_step_sync_config_done(user_input)

async def async_step_set_sync_name(
async def _async_step_sync_config_done(
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)
service = self._get_service()
if self._is_create:
await service.async_create_sync(self._sync, user_input)
else:
await service.async_modify_sync(self._sync, user_input)

# store config to integration options
if self._sync.config:
self._config[self._sync.topic] = self._sync.config
elif self._sync.topic in self._config:
self._config.pop(self._sync.topic)
return self.async_create_entry(title="", data={OPTIONS_CONFIG: self._config})

async def async_step_destroy_sync(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Destroy hass-to-bemfa sync(s)"""
service = self._get_service()
if user_input is not None:
for topic in user_input[OPTIONS_SELECT]:
await service.remove_topic(topic)
if topic in self._config:
self._config.pop(topic)
return self.async_create_entry(
title="", data={OPTIONS_CONFIG: self._config}
)

all_topics = await service.async_fetch_all_topics()
all_syncs = service.collect_supported_syncs()
topic_map: dict[str, str] = {}
for sync in all_syncs:
if sync.topic in all_topics:
sync.name = all_topics[sync.topic]
all_topics.pop(sync.topic)
topic_map[sync.topic] = sync.generate_option_label()

for (topic, name) in all_topics.items():
topic_map[topic] = "[?] {name}".format(name=name)

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

def _get_service(self) -> BemfaService:
return self.hass.data[DOMAIN].get(self._config_entry.entry_id)["service"]
return self.hass.data[DOMAIN].get(self._entry_id)["service"]
Loading

0 comments on commit f5b8e1a

Please sign in to comment.