├── README.md
├── custom_components
└── aqara_bridge
│ ├── __init__.py
│ ├── air_quality.py
│ ├── binary_sensor.py
│ ├── climate.py
│ ├── config_flow.py
│ ├── core
│ ├── aiot_cloud.py
│ ├── aiot_manager.py
│ ├── aiot_mapping.py
│ ├── const.py
│ └── utils.py
│ ├── cover.py
│ ├── light.py
│ ├── manifest.json
│ ├── remote.py
│ ├── sensor.py
│ ├── switch.py
│ └── translations
│ ├── en.json
│ ├── zh-Hans.json
│ └── zh-Hant.json
└── hacs.json
/README.md:
--------------------------------------------------------------------------------
1 | # Aqara Bridge for Home Assistant
2 |
3 | An integration of home-assistant which supports Aqara IoT Cloud.
4 |
5 | [](https://github.com/niceboygithub/aqara_bridge/releases/latest) [](https://github.com/niceboygithub/aqara_bridge/stargazers) [](https://github.com/niceboygithub/AqaraBridge/issues) [](https://hacs.xyz)
6 |
7 |
8 | First Step
9 |
10 | Apply developer account of [Aqara IoT Cloud](https://developer.aqara.com/register). Then wait for approved. It may takes for several days.
--------------------------------------------------------------------------------
/custom_components/aqara_bridge/__init__.py:
--------------------------------------------------------------------------------
1 | import datetime
2 | import re
3 | import logging
4 |
5 | from homeassistant.core import HomeAssistant
6 | from homeassistant.config_entries import ConfigEntry
7 | from homeassistant.helpers import aiohttp_client
8 |
9 | from .core.aiot_manager import (
10 | AiotManager,
11 | AiotDevice,
12 | )
13 | from .core.aiot_cloud import AiotCloud
14 | from .core.const import *
15 | from .core.utils import AqaraBridgeDebug
16 |
17 |
18 | _LOGGER = logging.getLogger(__name__)
19 |
20 |
21 | def data_masking(s: str, n: int) -> str:
22 | return re.sub(f"(?<=.{{{n}}}).(?=.{{{n}}})", "*", str(s))
23 |
24 |
25 | def gen_auth_entry(
26 | account: str, account_type: int, country_code: str, token_result: dict
27 | ):
28 | auth_entry = {}
29 | auth_entry[CONF_ENTRY_AUTH_ACCOUNT] = account
30 | auth_entry[CONF_ENTRY_AUTH_ACCOUNT_TYPE] = account_type
31 | auth_entry[CONF_ENTRY_AUTH_COUNTRY_CODE] = country_code
32 | auth_entry[CONF_ENTRY_AUTH_OPENID] = token_result["openId"]
33 | auth_entry[CONF_ENTRY_AUTH_ACCESS_TOKEN] = token_result["accessToken"]
34 | auth_entry[CONF_ENTRY_AUTH_EXPIRES_IN] = token_result["expiresIn"]
35 | auth_entry[CONF_ENTRY_AUTH_EXPIRES_TIME] = (
36 | datetime.datetime.now()
37 | + datetime.timedelta(seconds=int(token_result["expiresIn"]))
38 | ).strftime("%Y-%m-%d %H:%M:%S")
39 | auth_entry[CONF_ENTRY_AUTH_REFRESH_TOKEN] = token_result["refreshToken"]
40 | return auth_entry
41 |
42 |
43 | def init_hass_data(hass):
44 | hass.data.setdefault(DOMAIN, {})
45 | hass.data[DOMAIN].setdefault(HASS_DATA_AUTH_ENTRY_ID, None)
46 | session = AiotCloud(aiohttp_client.async_create_clientsession(hass))
47 | if not hass.data[DOMAIN].get(HASS_DATA_AIOTCLOUD):
48 | hass.data[DOMAIN].setdefault(HASS_DATA_AIOTCLOUD, session)
49 | if not hass.data[DOMAIN].get(HASS_DATA_AIOT_MANAGER):
50 | hass.data[DOMAIN].setdefault(HASS_DATA_AIOT_MANAGER, AiotManager(hass, session))
51 | hass.data[DOMAIN][CONF_DEBUG] = _LOGGER.level > 0 # default debug from Hass config
52 |
53 |
54 | async def async_setup(hass, config):
55 | """Setup component."""
56 | init_hass_data(hass)
57 | return True
58 |
59 |
60 | async def async_setup_entry(hass, entry):
61 | def token_updated(access_token, refresh_token):
62 | auth_entry = hass.data[DOMAIN][HASS_DATA_AUTH_ENTRY_ID]
63 | if auth_entry:
64 | data = auth_entry.data.copy()
65 | data[CONF_ENTRY_AUTH_ACCESS_TOKEN] = access_token
66 | data[CONF_ENTRY_AUTH_REFRESH_TOKEN] = refresh_token
67 | hass.config_entries.async_update_entry(entry, data=data)
68 |
69 | """Set up the Aqara components from a config entry."""
70 |
71 | if CONF_ENTRY_AUTH_ACCOUNT in entry.data:
72 | await _setup_logger(hass)
73 |
74 | # add update handler
75 | if not entry.update_listeners:
76 | entry.add_update_listener(async_update_options)
77 |
78 | data = entry.data.copy()
79 | manager: AiotManager = hass.data[DOMAIN][HASS_DATA_AIOT_MANAGER]
80 | if CONF_ENTRY_AUTH_ACCOUNT in entry.data:
81 | aiotcloud: AiotCloud = hass.data[DOMAIN][HASS_DATA_AIOTCLOUD]
82 | aiotcloud.set_options(entry.options)
83 | aiotcloud.update_token_event_callback = token_updated
84 | if (
85 | datetime.datetime.strptime(
86 | data.get(CONF_ENTRY_AUTH_EXPIRES_TIME), "%Y-%m-%d %H:%M:%S"
87 | )
88 | <= datetime.datetime.now()
89 | ):
90 | resp = aiotcloud.async_refresh_token(
91 | data.get(CONF_ENTRY_AUTH_REFRESH_TOKEN)
92 | )
93 | if isinstance(resp, dict) and resp["code"] == 0:
94 | auth_entry = gen_auth_entry(
95 | data.get(CONF_ENTRY_AUTH_ACCOUNT),
96 | data.get(CONF_ENTRY_AUTH_ACCOUNT_TYPE),
97 | data.get(CONF_ENTRY_AUTH_COUNTRY_CODE),
98 | resp["result"],
99 | )
100 | hass.config_entries.async_update_entry(entry, data=auth_entry)
101 | else:
102 | # TODO 这里需要处理刷新令牌失败的情况
103 | return False
104 | else:
105 | aiotcloud.set_country(data.get(CONF_ENTRY_AUTH_COUNTRY_CODE))
106 | aiotcloud.access_token = data.get(CONF_ENTRY_AUTH_ACCESS_TOKEN)
107 | aiotcloud.refresh_token = data.get(CONF_ENTRY_AUTH_REFRESH_TOKEN)
108 |
109 | hass.data[DOMAIN][HASS_DATA_AUTH_ENTRY_ID] = entry.entry_id
110 | await manager.async_refresh_all_devices()
111 | else:
112 | await manager.async_add_devices(entry, [AiotDevice(**entry.data)], True)
113 | await manager.async_forward_entry_setup(entry)
114 |
115 | return True
116 |
117 |
118 | async def async_unload_entry(hass, entry):
119 | # if CONF_ENTRY_AUTH_ACCOUNT in entry.data:
120 | # hass.data[DOMAIN][HASS_DATA_AUTH_ENTRY_ID] = None
121 | # else:
122 | # manager: AiotManager = hass.data[DOMAIN][HASS_DATA_AIOT_MANAGER]
123 | # await manager.async_unload_entry(entry)
124 | return True
125 |
126 |
127 | async def async_remove_entry(hass, entry):
128 | if CONF_ENTRY_AUTH_ACCOUNT in entry.data:
129 | hass.data[DOMAIN][HASS_DATA_AUTH_ENTRY_ID] = None
130 | else:
131 | manager: AiotManager = hass.data[DOMAIN][HASS_DATA_AIOT_MANAGER]
132 | await manager.async_remove_entry(entry)
133 | return True
134 |
135 |
136 | async def async_update_options(hass: HomeAssistant, entry: ConfigEntry):
137 | """ Update Optioins if available """
138 | await hass.config_entries.async_reload(entry.entry_id)
139 |
140 |
141 | # These code are reference to @AlexxIT. It is very useful to help debug.
142 | async def _setup_logger(hass: HomeAssistant):
143 | entries = hass.config_entries.async_entries(DOMAIN)
144 | any_debug = any(e.options.get(CONF_DEBUG) for e in entries)
145 |
146 | # only if global logging don't set
147 | if not hass.data[DOMAIN][CONF_DEBUG]:
148 | # disable log to console
149 | _LOGGER.propagate = not any_debug
150 | # set debug if any of integrations has debug
151 | _LOGGER.setLevel(logging.DEBUG if any_debug else logging.NOTSET)
152 |
153 | # if don't set handler yet
154 | if any_debug and not _LOGGER.handlers:
155 | handler = AqaraBridgeDebug(hass)
156 | _LOGGER.addHandler(handler)
157 |
158 | info = await hass.helpers.system_info.async_get_system_info()
159 | info.pop('timezone')
160 | _LOGGER.debug(f"SysInfo: {info}")
--------------------------------------------------------------------------------
/custom_components/aqara_bridge/air_quality.py:
--------------------------------------------------------------------------------
1 | """Support for Aqara Air Quality Monitor."""
2 | from homeassistant.components.air_quality import AirQualityEntity
3 | from homeassistant.const import (
4 | ATTR_TEMPERATURE
5 | )
6 |
7 | from .core.aiot_manager import (
8 | AiotManager,
9 | AiotEntityBase,
10 | )
11 | from .core.const import (
12 | ATTR_CO2E,
13 | ATTR_HUMIDITY,
14 | ATTR_TVOC,
15 | DOMAIN,
16 | HASS_DATA_AIOT_MANAGER,
17 | PROP_TO_ATTR_BASE
18 | )
19 |
20 | TYPE = "air_quality"
21 |
22 | DATA_KEY = f"{TYPE}.{DOMAIN}"
23 |
24 | PROP_TO_ATTR = {
25 | "temperature": ATTR_TEMPERATURE,
26 | "humidity": ATTR_HUMIDITY
27 | }
28 |
29 | PROP_TO_ATTR_TVOC = {
30 | "tvoc_level": ATTR_TVOC,
31 | }
32 |
33 | PROP_TO_ATTR_CO2E = {
34 | "carbon_dioxide_equivalent": ATTR_CO2E,
35 | }
36 |
37 | async def async_setup_entry(hass, config_entry, async_add_entities):
38 | manager: AiotManager = hass.data[DOMAIN][HASS_DATA_AIOT_MANAGER]
39 | cls_entities = {
40 | "tvoc_level": AiotTvocEntity,
41 | "default": AiotAirMonitorEntity
42 | }
43 | await manager.async_add_entities(
44 | config_entry, TYPE, cls_entities, async_add_entities
45 | )
46 |
47 |
48 | class AiotAirMonitorEntity(AiotEntityBase, AirQualityEntity):
49 | """ Air Monitor Entity"""
50 | def __init__(self, hass, device, res_params, channel=None, **kwargs):
51 | AiotEntityBase.__init__(self, hass, device, res_params, TYPE, channel, **kwargs)
52 | self._attr_state_class = kwargs.get("state_class")
53 | self._attr_name = f"{self._attr_name} {self._attr_device_class}"
54 | self._attr_temperature = None
55 | self._attr_humidity = None
56 | self._attr_particulate_matter_2_5 = None
57 | self._attr_particulate_matter_0_1 = None
58 | self._attr_particulate_matter_1_0 = None
59 |
60 | @property
61 | def carbon_dioxide_equivalent(self):
62 | """Return the CO2e (carbon dioxide equivalent) level."""
63 | return self._attr_carbon_dioxide_equivalent
64 |
65 | @property
66 | def temperature(self):
67 | """Return the current temperature."""
68 | return self._attr_temperature
69 |
70 | @property
71 | def humidity(self):
72 | """Return the current humidity."""
73 | return self._attr_humidity
74 |
75 | @property
76 | def particulate_matter_0_1(self):
77 | """Return the particulate matter 0.1 level."""
78 | return self._attr_particulate_matter_0_1
79 |
80 | @property
81 | def particulate_matter_2_5(self):
82 | """Return the particulate matter 2.5 level."""
83 | return self._attr_particulate_matter_2_5
84 |
85 | @property
86 | def particulate_matter_10(self):
87 | """Return the particulate matter 10 level."""
88 | return self._attr_particulate_matter_1_0
89 |
90 | def convert_res_to_attr(self, res_name, res_value):
91 | if res_name == "chip_temperature":
92 | return round(float(res_value), 1)
93 | if res_name == "fw_ver":
94 | return res_value
95 | if res_name == "lqi":
96 | return int(res_value)
97 | if res_name == "voltage":
98 | return format(float(res_value) / 1000, '.3f')
99 | if res_name == "co2e":
100 | return round(float(res_value), 1)
101 | if res_name == "temperature":
102 | return round(float(res_value), 1)
103 | if res_name == "humidity":
104 | return round(float(res_value / 100), 1)
105 | return super().convert_res_to_attr(res_name, res_value)
106 |
107 | @property
108 | def extra_state_attributes(self):
109 | """Return the optional state attributes."""
110 | data = {}
111 |
112 | for prop, attr in PROP_TO_ATTR_BASE.items():
113 | value = getattr(self, prop)
114 | if value is not None:
115 | data[attr] = value
116 |
117 | for prop, attr in PROP_TO_ATTR.items():
118 | value = getattr(self, prop)
119 | if value is not None:
120 | data[attr] = value
121 |
122 | return data
123 |
124 |
125 | class AiotTvocEntity(AiotAirMonitorEntity, AirQualityEntity):
126 | """Air Quality class for Aqara TVOC device."""
127 | _attr_tvoc_level = None
128 |
129 | @property
130 | def tvoc_level(self):
131 | """Return the total volatile organic compounds."""
132 | return self._attr_tvoc_level
133 |
134 | @property
135 | def extra_state_attributes(self):
136 | """Return the optional state attributes."""
137 | data = {}
138 |
139 | for prop, attr in PROP_TO_ATTR_BASE.items():
140 | value = getattr(self, prop)
141 | if value is not None:
142 | data[attr] = value
143 |
144 | for prop, attr in PROP_TO_ATTR.items():
145 | value = getattr(self, prop)
146 | if value is not None:
147 | data[attr] = value
148 |
149 | for prop, attr in PROP_TO_ATTR_TVOC.items():
150 | value = getattr(self, prop)
151 | if value is not None:
152 | data[attr] = value
153 |
154 | return data
155 |
156 | def convert_res_to_attr(self, res_name, res_value):
157 | if res_name == "chip_temperature":
158 | return round(float(res_value), 1)
159 | if res_name == "fw_ver":
160 | return res_value
161 | if res_name == "lqi":
162 | return int(res_value)
163 | if res_name == "voltage":
164 | return format(float(res_value) / 1000, '.3f')
165 | if res_name == "tvoc_level":
166 | return int(res_value)
167 | if res_name == "temperature":
168 | return round(float(res_value), 1)
169 | if res_name == "humidity":
170 | return round(float(res_value), 1)
171 | return super().convert_res_to_attr(res_name, res_value)
172 |
173 |
--------------------------------------------------------------------------------
/custom_components/aqara_bridge/binary_sensor.py:
--------------------------------------------------------------------------------
1 | """Support for Xiaomi Aqara binary sensors."""
2 | import time
3 |
4 | from homeassistant.config import DATA_CUSTOMIZE
5 | from homeassistant.helpers.event import async_call_later
6 | from homeassistant.components.binary_sensor import BinarySensorEntity
7 |
8 | from .core.aiot_manager import (
9 | AiotManager,
10 | AiotEntityBase,
11 | )
12 | from .core.const import (
13 | CONF_OCCUPANCY_TIMEOUT,
14 | DOMAIN,
15 | HASS_DATA_AIOT_MANAGER,
16 | PROP_TO_ATTR_BASE
17 | )
18 |
19 | TYPE = "binary_sensor"
20 |
21 | DATA_KEY = f"{TYPE}.{DOMAIN}"
22 |
23 |
24 | async def async_setup_entry(hass, config_entry, async_add_entities):
25 | manager: AiotManager = hass.data[DOMAIN][HASS_DATA_AIOT_MANAGER]
26 | cls_entities = {
27 | "motion": AiotMotionBinarySensor,
28 | "contact": AiotDoorBinarySensor,
29 | "default": AiotBinarySensorEntity
30 | }
31 | await manager.async_add_entities(
32 | config_entry, TYPE, cls_entities, async_add_entities
33 | )
34 |
35 |
36 | class AiotBinarySensorEntity(AiotEntityBase, BinarySensorEntity):
37 | def __init__(self, hass, device, res_params, channel=None, **kwargs):
38 | AiotEntityBase.__init__(self, hass, device, res_params, TYPE, channel, **kwargs)
39 | self._attr_state_class = kwargs.get("state_class")
40 | self._attr_name = f"{self._attr_name} {self._attr_device_class}"
41 |
42 | def convert_res_to_attr(self, res_name, res_value):
43 | if res_name == "chip_temperature":
44 | return round(float(res_value), 1)
45 | if res_name == "fw_ver":
46 | return res_value
47 | if res_name == "lqi":
48 | return int(res_value)
49 | if res_name == "voltage":
50 | return format(float(res_value) / 1000, '.3f')
51 | return super().convert_res_to_attr(res_name, res_value)
52 |
53 | @property
54 | def extra_state_attributes(self):
55 | """Return the optional state attributes."""
56 | data = {}
57 |
58 | for prop, attr in PROP_TO_ATTR_BASE.items():
59 | value = getattr(self, prop)
60 | if value is not None:
61 | data[attr] = value
62 |
63 | return data
64 |
65 | class AiotMotionBinarySensor(AiotBinarySensorEntity, BinarySensorEntity):
66 | def __init__(self, hass, device, res_params, channel=None, **kwargs):
67 | AiotEntityBase.__init__(self, hass, device, res_params, TYPE, channel, **kwargs)
68 | self._attr_state_class = kwargs.get("state_class")
69 | self._attr_name = f"{self._attr_name} {self._attr_device_class}"
70 | self._default_delay = 120
71 | self._last_on = 0
72 | self._last_off = 0
73 | self._timeout_pos = 0
74 | self._unsub_set_no_motion = None
75 | self._attr_is_on = False
76 |
77 | async def _start_no_motion_timer(self, delay: float):
78 | if self._unsub_set_no_motion:
79 | self._unsub_set_no_motion()
80 |
81 | self._unsub_set_no_motion = async_call_later(
82 | self.hass, abs(delay), self._set_no_motion)
83 |
84 | async def _set_no_motion(self, *args):
85 | self._last_off = time.time()
86 | self._timeout_pos = 0
87 | self._unsub_set_no_motion = None
88 | self._attr_is_on = False
89 | self.schedule_update_ha_state()
90 |
91 | # repeat event from Aqara integration
92 | self.hass.bus.fire('xiaomi_aqara.motion', {
93 | 'entity_id': self.entity_id
94 | })
95 |
96 | def convert_res_to_attr(self, res_name, res_value):
97 | if res_name == "chip_temperature":
98 | return format((int(res_value) - 32) * 5 / 9, '.2f')
99 | if res_name == "fw_ver":
100 | return res_value
101 | if res_name == "lqi":
102 | return int(res_value)
103 | if res_name == "voltage":
104 | return format(float(res_value) / 1000, '.3f')
105 |
106 | time_now = time.time()
107 |
108 | if time_now - self._last_on < 1:
109 | return
110 | self._attr_is_on = bool(res_value)
111 | self._last_on = time_now
112 |
113 | # handle available change
114 | self.schedule_update_ha_state()
115 |
116 | if self._unsub_set_no_motion:
117 | self._unsub_set_no_motion()
118 |
119 | custom = self.hass.data[DATA_CUSTOMIZE].get(self.entity_id)
120 | # if customize of any entity will be changed from GUI - default value
121 | # for all motion sensors will be erased
122 | timeout = custom.get(CONF_OCCUPANCY_TIMEOUT, self._default_delay)
123 | if timeout:
124 | if isinstance(timeout, list):
125 | pos = min(self._timeout_pos, len(timeout) - 1)
126 | delay = timeout[pos]
127 | self._timeout_pos += 1
128 | else:
129 | delay = timeout
130 |
131 | if delay < 0 and time_now + delay < self._last_off:
132 | delay *= 2
133 | self.hass.add_job(self._start_no_motion_timer, delay)
134 |
135 | # repeat event from Aqara integration
136 | self.hass.bus.fire('xiaomi_aqara.motion', {
137 | 'entity_id': self.entity_id
138 | })
139 | return bool(res_value)
140 |
141 |
142 | class AiotDoorBinarySensor(AiotBinarySensorEntity, BinarySensorEntity):
143 | def convert_res_to_attr(self, res_name, res_value):
144 | if res_name == "chip_temperature":
145 | return round(float(res_value), 1)
146 | if res_name == "fw_ver":
147 | return res_value
148 | if res_name == "lqi":
149 | return int(res_value)
150 | if res_name == "voltage":
151 | return format(float(res_value) / 1000, '.3f')
152 |
153 | self._attr_is_on = not bool(res_value)
154 | self.schedule_update_ha_state()
155 | return not bool(res_value)
156 |
--------------------------------------------------------------------------------
/custom_components/aqara_bridge/climate.py:
--------------------------------------------------------------------------------
1 | import logging
2 | from homeassistant.components.climate import ClimateEntity
3 |
4 | from .core.aiot_manager import (
5 | AiotManager,
6 | AiotEntityBase,
7 | )
8 | from .core.aiot_mapping import SPECIAL_DEVICES_INFO
9 | from .core.const import DOMAIN, HASS_DATA_AIOT_MANAGER
10 |
11 | TYPE = "climate"
12 |
13 | _LOGGER = logging.getLogger(__name__)
14 |
15 | DATA_KEY = f"{TYPE}.{DOMAIN}"
16 |
17 |
18 | AC_STATE_MAPPING = {
19 | "hvac_mode": {
20 | "off": [{"code": "0000", "start": 0, "end": 4}],
21 | "heat": [
22 | {"code": "0001", "start": 0, "end": 4},
23 | {"code": "0000", "start": 4, "end": 8},
24 | ],
25 | "cool": [
26 | {"code": "0001", "start": 0, "end": 4},
27 | {"code": "0001", "start": 4, "end": 8},
28 | ],
29 | },
30 | "fan_mode": {
31 | "low": [{"code": "0000", "start": 8, "end": 12}],
32 | "middle": [{"code": "0001", "start": 8, "end": 12}],
33 | "high": [{"code": "0010", "start": 8, "end": 12}],
34 | },
35 | "temperature": {"0": [{"start": 16, "end": 24}]},
36 | }
37 |
38 |
39 | async def async_setup_entry(hass, config_entry, async_add_entities):
40 | manager: AiotManager = hass.data[DOMAIN][HASS_DATA_AIOT_MANAGER]
41 | cls_entities = {
42 | "default": AiotClimateEntity
43 | }
44 | await manager.async_add_entities(
45 | config_entry, TYPE, cls_entities, async_add_entities
46 | )
47 |
48 |
49 | class AiotClimateEntity(AiotEntityBase, ClimateEntity):
50 | """VRF空调控制器,特殊资源定义,https://opendoc.aqara.cn/docs/%E4%BA%91%E5%AF%B9%E6%8E%A5%E5%BC%80%E5%8F%91%E6%89%8B%E5%86%8C/%E9%99%84%E5%BD%95/%E7%89%B9%E6%AE%8A%E8%B5%84%E6%BA%90%E5%AE%9A%E4%B9%89.html"""
51 |
52 | def __init__(self, hass, device, res_params, channel=None, **kwargs):
53 | AiotEntityBase.__init__(self, hass, device, res_params, TYPE, channel, **kwargs)
54 | self._attr_hvac_modes = kwargs.get("hvac_modes")
55 | self._attr_temperature_unit = kwargs.get("unit_of_measurement")
56 | self._attr_target_temperature_step = kwargs.get("target_temp_step")
57 | self._attr_fan_modes = kwargs.get("fan_modes")
58 | self._attr_min_temp = kwargs.get("min_temp")
59 | self._attr_max_temp = kwargs.get("max_temp")
60 | self._state_str = "".zfill(32)
61 |
62 | async def _async_change_ac_state(self, attr_name, attr_value, fix_code=None):
63 | mappings = AC_STATE_MAPPING[attr_name].get(attr_value)
64 | new_state_str = self._state_str
65 | if mappings:
66 | for mapping in mappings:
67 | code = fix_code if fix_code else mapping["code"]
68 | start = mapping["start"]
69 | end = mapping["end"]
70 | new_state_str = f"{new_state_str[0:start]}{code}{new_state_str[end:]}"
71 |
72 | await self.async_set_resource("ac_state", new_state_str)
73 | else:
74 | _LOGGER.warn(f"Attr value '{attr_value}' is not supported in {attr_name}")
75 |
76 | async def async_set_fan_mode(self, fan_mode: str):
77 | await self._async_change_ac_state("fan_mode", fan_mode)
78 |
79 | async def async_set_hvac_mode(self, hvac_mode: str):
80 | await self._async_change_ac_state("hvac_mode", hvac_mode)
81 |
82 | async def async_set_temperature(self, **kwargs):
83 | temperature = kwargs.get("temperature")
84 | await self._async_change_ac_state(
85 | "temperature", "0", bin(int(temperature))[2:].zfill(8)
86 | )
87 |
88 | def convert_attr_to_res(self, res_name, value):
89 | if res_name == "ac_state":
90 | # res_value:二进制字符串
91 | return int(value, 2)
92 | return super().convert_attr_to_res(res_name, value)
93 |
94 | def convert_res_to_attr(self, res_name, res_value):
95 | if res_name == "ac_state":
96 | # res_value: 十进制字符串
97 | return bin(int(res_value))[2:].zfill(32)
98 | return super().convert_res_to_attr(res_name, res_value)
99 |
100 | def __setattr__(self, name: str, value):
101 | if name == "_state_str" and value != "".zfill(32):
102 | sdi = SPECIAL_DEVICES_INFO[self._device.model]
103 | self._attr_hvac_mode = sdi["hvac_mode"][int(value[4:8], 2)]
104 | if int(value[0:4], 2) == 0:
105 | self._attr_hvac_mode = "off"
106 | self._attr_fan_mode = sdi["fan_mode"][int(value[8:12], 2)]
107 | self._attr_target_temperature = int(value[16:24], 2)
108 | return super().__setattr__(name, value)
109 |
--------------------------------------------------------------------------------
/custom_components/aqara_bridge/config_flow.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import voluptuous as vol
3 |
4 | import homeassistant.helpers.config_validation as cv
5 | from homeassistant.config_entries import (
6 | CONN_CLASS_LOCAL_PUSH,
7 | ConfigFlow,
8 | OptionsFlow,
9 | ConfigEntry
10 | )
11 | from homeassistant.core import callback
12 |
13 | from . import init_hass_data, data_masking, gen_auth_entry
14 | from .core.const import (
15 | DOMAIN,
16 | CONF_FIELD_ACCOUNT,
17 | CONF_FIELD_COUNTRY_CODE,
18 | CONF_FIELD_AUTH_CODE,
19 | CONF_FIELD_SELECTED_DEVICES,
20 | CONF_FIELD_REFRESH_TOKEN,
21 | CONF_ENTRY_AUTH_ACCOUNT,
22 | HASS_DATA_AIOTCLOUD,
23 | HASS_DATA_AIOT_MANAGER,
24 | SERVER_COUNTRY_CODES,
25 | SERVER_COUNTRY_CODES_DEFAULT,
26 | CONF_ENTRY_AUTH_ACCOUNT,
27 | HASS_DATA_AUTH_ENTRY_ID,
28 | CONF_DEBUG,
29 | CONF_STATS,
30 | OPT_DEBUG
31 | )
32 |
33 | _LOGGER = logging.getLogger(__name__)
34 |
35 | DEVICE_GET_AUTH_CODE_CONFIG = vol.Schema(
36 | {
37 | vol.Required(CONF_FIELD_ACCOUNT): str,
38 | vol.Required(
39 | CONF_FIELD_COUNTRY_CODE, default=SERVER_COUNTRY_CODES_DEFAULT
40 | ): vol.In(SERVER_COUNTRY_CODES),
41 | vol.Optional(CONF_FIELD_REFRESH_TOKEN): str,
42 | }
43 | )
44 |
45 | DEVICE_GET_TOKEN_CONFIG = vol.Schema({vol.Required(CONF_FIELD_AUTH_CODE): str})
46 |
47 |
48 | class AqaraBridgeFlowHandler(ConfigFlow, domain=DOMAIN):
49 | """Handle an Aqara Bridge config flow."""
50 |
51 | VERSION = 1
52 |
53 | def __init__(self):
54 | """Initialize."""
55 | self.account = None
56 | self.country_code = None
57 | self.account_type = None
58 | self._session = None
59 | self._device_manager = None
60 |
61 | @staticmethod
62 | @callback
63 | def async_get_options_flow(config_entry: ConfigEntry):
64 | """ get option flow """
65 | if CONF_ENTRY_AUTH_ACCOUNT in config_entry.data:
66 | return OptionsFlowHandler(config_entry)
67 | return DeviceOptionsFlowHandler(config_entry)
68 |
69 | async def async_step_user(self, user_input=None):
70 | """Handle a flow initialized by the user."""
71 | init_hass_data(self.hass)
72 | self._device_manager = self.hass.data[DOMAIN][HASS_DATA_AIOT_MANAGER]
73 | auth_entry_id = self.hass.data[DOMAIN][HASS_DATA_AUTH_ENTRY_ID]
74 | self._session = self.hass.data[DOMAIN][HASS_DATA_AIOTCLOUD]
75 | if auth_entry_id:
76 | return await self.async_step_select_devices()
77 | else:
78 | # self._session = AiotCloud(
79 | # aiohttp_client.async_create_clientsession(self.hass)
80 | # )
81 | # self.hass.data[DOMAIN][HASS_DATA_AIOTCLOUD] = self._session
82 | return await self.async_step_get_auth_code()
83 |
84 | async def async_step_get_auth_code(self, user_input=None):
85 | """Configure an aqara device through the Aqara Cloud."""
86 | errors = {}
87 | if user_input:
88 | self.account = user_input.get(CONF_FIELD_ACCOUNT)
89 | self.country_code = user_input.get(CONF_FIELD_COUNTRY_CODE)
90 | self.account_type = 0
91 | self._session.set_country(self.country_code)
92 |
93 | refresh_token = user_input.get(CONF_FIELD_REFRESH_TOKEN)
94 | if refresh_token and refresh_token != "":
95 | resp = await self._session.async_refresh_token(refresh_token)
96 | if resp["code"] == 0:
97 | auth_entry = gen_auth_entry(
98 | self.account,
99 | self.account_type,
100 | self.country_code,
101 | resp["result"],
102 | )
103 | self.hass.async_add_job(
104 | self.hass.config_entries.flow.async_init(
105 | DOMAIN, context={"source": "get_token"}, data=auth_entry
106 | )
107 | )
108 | return await self.async_step_select_devices()
109 | else:
110 | # TODO 这里要处理API失败的情况
111 | pass
112 | else:
113 | resp = await self._session.async_get_auth_code(self.account, 0)
114 | if resp["code"] == 0:
115 | return await self.async_step_get_token()
116 | else:
117 | # TODO 这里要处理API失败的情况
118 | pass
119 |
120 | return self.async_show_form(
121 | step_id="get_auth_code",
122 | data_schema=DEVICE_GET_AUTH_CODE_CONFIG,
123 | errors=errors,
124 | )
125 |
126 | async def async_step_get_token(self, user_input=None):
127 | errors = {}
128 | if user_input:
129 | if CONF_FIELD_AUTH_CODE in user_input:
130 | auth_code = user_input.get(CONF_FIELD_AUTH_CODE)
131 | resp = await self._session.async_get_token(auth_code, self.account, 0)
132 |
133 | if resp["code"] == 0:
134 | auth_entry = gen_auth_entry(
135 | self.account,
136 | self.account_type,
137 | self.country_code,
138 | resp["result"],
139 | )
140 | self.hass.async_add_job(
141 | self.hass.config_entries.flow.async_init(
142 | DOMAIN, context={"source": "get_token"}, data=auth_entry
143 | )
144 | )
145 | else:
146 | errors["base"] = "cloud_credentials_incomplete"
147 | elif CONF_ENTRY_AUTH_ACCOUNT in user_input:
148 | return self.async_create_entry(
149 | title=data_masking(user_input[CONF_ENTRY_AUTH_ACCOUNT], 4),
150 | data=user_input,
151 | )
152 |
153 | return await self.async_step_select_devices()
154 |
155 | return self.async_show_form(
156 | step_id="get_token", data_schema=DEVICE_GET_TOKEN_CONFIG, errors=errors
157 | )
158 |
159 | async def async_step_select_devices(self, user_input=None):
160 | errors = {}
161 | if user_input:
162 | if CONF_FIELD_SELECTED_DEVICES in user_input:
163 | dids = user_input[CONF_FIELD_SELECTED_DEVICES]
164 | devices = await self._session.async_query_device_info(dids)
165 | for device in devices:
166 | self.hass.async_add_job(
167 | self.hass.config_entries.flow.async_init(
168 | DOMAIN, context={"source": "select_devices"}, data=device
169 | )
170 | )
171 | elif "did" in user_input:
172 | await self.async_set_unique_id(
173 | user_input["did"], raise_on_progress=False
174 | )
175 | return self.async_create_entry(
176 | title=user_input["deviceName"], data=user_input
177 | )
178 |
179 | return self.async_abort(reason="complete")
180 |
181 | devlist = {}
182 | await self._device_manager.async_refresh_all_devices() # 刷新一下
183 | [
184 | devlist.setdefault(x.did, f"{x.device_name} - {x.model}")
185 | for x in self._device_manager.unmanaged_gateways
186 | ]
187 | return self.async_show_form(
188 | step_id="select_devices",
189 | data_schema=vol.Schema(
190 | {
191 | vol.Required(
192 | CONF_FIELD_SELECTED_DEVICES, default=[]
193 | ): cv.multi_select(devlist)
194 | }
195 | ),
196 | errors=errors,
197 | )
198 |
199 | class OptionsFlowHandler(OptionsFlow):
200 | # pylint: disable=too-few-public-methods
201 | """Handle options flow changes."""
202 | _account = None
203 | _token = None
204 |
205 | def __init__(self, config_entry):
206 | """Initialize options flow."""
207 | self.config_entry = config_entry
208 |
209 | async def async_step_init(self, user_input=None):
210 | """Manage options."""
211 | if user_input is not None:
212 | return self.async_create_entry(
213 | title='',
214 | data={
215 | CONF_DEBUG: user_input.get(CONF_DEBUG, []),
216 | },
217 | )
218 | debug = self.config_entry.options.get(CONF_DEBUG, [])
219 |
220 | return self.async_show_form(
221 | step_id="init",
222 | data_schema=vol.Schema(
223 | {
224 | vol.Optional(CONF_DEBUG, default=debug): cv.multi_select(
225 | OPT_DEBUG
226 | ),
227 | }
228 | ),
229 | )
230 |
231 |
232 | class DeviceOptionsFlowHandler(OptionsFlow):
233 | """Handle options flow changes."""
234 |
235 | def __init__(self, config_entry):
236 | """Initialize options flow."""
237 | self.config_entry = config_entry
238 |
239 | async def async_step_init(self, user_input=None):
240 | """Manage options."""
241 | if user_input is not None:
242 | return self.async_create_entry(
243 | title='',
244 | data={
245 | CONF_STATS: user_input.get(CONF_STATS, False)
246 | },
247 | )
248 | stats = self.config_entry.options.get(CONF_STATS, False)
249 |
250 | return self.async_show_form(
251 | step_id="init",
252 | data_schema=vol.Schema(
253 | {
254 | vol.Optional(CONF_STATS, default=stats): bool
255 | }
256 | ),
257 | )
258 |
--------------------------------------------------------------------------------
/custom_components/aqara_bridge/core/aiot_cloud.py:
--------------------------------------------------------------------------------
1 | import hashlib
2 | import json
3 | import random
4 | import string
5 | import time
6 | import logging
7 |
8 | from aiohttp import ClientSession
9 |
10 | _LOGGER = logging.getLogger(__name__)
11 |
12 | API_DOMAIN = {
13 | "CN": "open-cn.aqara.com",
14 | "USA": "open-usa.aqara.com",
15 | "KR": "open-kr.aqara.com",
16 | "RU": "open-ru.aqara.com",
17 | "GER": "open-ger.aqara.com",
18 | }
19 |
20 | APP_ID = "88110776288481280040ace0"
21 | KEY_ID = "K.881107763014836224"
22 | APP_KEY = "t7g6qhx4nmbeqmfq1w6yksucnbrofsgs"
23 |
24 |
25 | def get_random_string(length: int):
26 | seq = string.ascii_uppercase + string.digits
27 | return "".join((random.choice(seq) for _ in range(length)))
28 |
29 |
30 | # 生成Headers中的sign
31 | def gen_sign(
32 | access_token: str,
33 | app_id: str,
34 | key_id: str,
35 | nonce: str,
36 | timestamp: str,
37 | app_key: str,
38 | ):
39 | """Signature in headers, see https://opendoc.aqara.cn/docs/%E4%BA%91%E5%AF%B9%E6%8E%A5%E5%BC%80%E5%8F%91%E6%89%8B%E5%86%8C/API%E4%BD%BF%E7%94%A8%E6%8C%87%E5%8D%97/Sign%E7%94%9F%E6%88%90%E8%A7%84%E5%88%99.html"""
40 | s = f"Appid={app_id}&Keyid={key_id}&Nonce={nonce}&Time={timestamp}{app_key}"
41 | if access_token and len(access_token) > 0:
42 | s = f"AccessToken={access_token}&{s}"
43 | s = s.lower()
44 | sign = hashlib.md5(s.encode("utf-8")).hexdigest()
45 | return sign
46 |
47 |
48 | class AiotCloud:
49 | access_token = None
50 | refresh_token = None
51 | update_token_event_callback = None
52 |
53 | def __init__(self, session: ClientSession):
54 | self.app_id = APP_ID
55 | self.key_id = KEY_ID
56 | self.app_key = APP_KEY
57 | self.session = session
58 | self.options = None
59 | self.set_country("CN")
60 |
61 | def set_options(self, options):
62 | """ set hass options """
63 | self.options = options
64 |
65 | def get_options(self):
66 | """ get hass options """
67 | return self.options
68 |
69 | def set_country(self, country: str):
70 | """ set aiot country """
71 | self.country = country
72 | self.api_url = f"https://{API_DOMAIN[country]}/v3.0/open/api"
73 |
74 | def _get_request_headers(self):
75 | """生成Headers"""
76 | nonce = get_random_string(16)
77 | timestamp = str(int(round(time.time() * 1000)))
78 | sign = gen_sign(
79 | self.access_token, self.app_id, self.key_id, nonce, timestamp, self.app_key
80 | )
81 | headers = {
82 | "Content-Type": "application/json",
83 | "Appid": self.app_id,
84 | "Keyid": self.key_id,
85 | "Nonce": nonce,
86 | "Time": timestamp,
87 | "Sign": sign,
88 | "Lang": "zh",
89 | }
90 | if self.access_token:
91 | headers["Accesstoken"] = self.access_token
92 | return headers
93 |
94 | async def _async_invoke_aqara_cloud_api(
95 | self, intent: str, only_result: bool = True, list_data: bool = False, **kwargs
96 | ):
97 | """调用Aqara Api"""
98 | try:
99 | empty_keys = []
100 | for k, v in kwargs.items():
101 | if v is None:
102 | empty_keys.append(k)
103 | [kwargs.pop(x) for x in empty_keys]
104 | payload = (
105 | {"intent": intent, "data": [kwargs]}
106 | if list_data
107 | else {"intent": intent, "data": kwargs}
108 | )
109 | r = await self.session.post(
110 | url=self.api_url,
111 | data=json.dumps(payload),
112 | headers=self._get_request_headers(),
113 | )
114 | raw = await r.read()
115 | jo = json.loads(raw)
116 |
117 | if only_result:
118 | # 这里的异常处理需要优化
119 | if jo["code"] != 0:
120 | # 调用Aiot api失败,返回值
121 | _LOGGER.warn(f"Call Aiot api failed,return:{jo}")
122 | if jo["code"] == 108:
123 | # 令牌过期或异常,正在尝试自动刷新
124 | _LOGGER.warn(f"Aiot token expired, trying to auto refresh!")
125 | new_jo = await self.async_refresh_token(self.refresh_token)
126 | if new_jo["code"] == 0:
127 | # Aiot令牌更新成功!
128 | _LOGGER.info(f"Aiot token refresh successfully!")
129 | return await self._async_invoke_aqara_cloud_api(
130 | intent, only_result, list_data, **kwargs
131 | )
132 | else:
133 | # Aiot令牌更新失败,请重新授权
134 | _LOGGER.warn("Aiot token refresh failed, please do authorization again!")
135 | return jo.get("result")
136 | else:
137 | return jo
138 |
139 | except Exception as ex:
140 | _LOGGER.error(ex)
141 |
142 | async def async_get_auth_code(
143 | self, account: str, account_type: int, access_token_validity: str = "7d"
144 | ):
145 | """获取授权验证码"""
146 | return await self._async_invoke_aqara_cloud_api(
147 | intent="config.auth.getAuthCode",
148 | only_result=False,
149 | account=account,
150 | accountType=account_type,
151 | accessTokenValidity=access_token_validity,
152 | )
153 |
154 | async def async_get_token(self, authCode: str, account: str, account_type: int):
155 | """获取访问令牌"""
156 | jo = await self._async_invoke_aqara_cloud_api(
157 | intent="config.auth.getToken",
158 | only_result=False,
159 | authCode=authCode,
160 | account=account,
161 | accountType=account_type,
162 | )
163 | if jo["code"] == 0:
164 | self.access_token = jo["result"]["accessToken"]
165 | self.refresh_token = jo["result"]["refreshToken"]
166 | if self.update_token_event_callback:
167 | self.update_token_event_callback(self.access_token, self.refresh_token)
168 |
169 | return jo
170 |
171 | async def async_refresh_token(self, refresh_token: str):
172 | """刷新访问令牌"""
173 | jo = await self._async_invoke_aqara_cloud_api(
174 | intent="config.auth.refreshToken",
175 | only_result=False,
176 | refreshToken=refresh_token,
177 | )
178 | if jo["code"] == 0:
179 | self.access_token = jo["result"]["accessToken"]
180 | self.refresh_token = jo["result"]["refreshToken"]
181 | if self.update_token_event_callback:
182 | self.update_token_event_callback(self.access_token, self.refresh_token)
183 |
184 | return jo
185 |
186 | async def async_query_device_sub_info(self, did: str):
187 | """获取设备入网bindKey"""
188 | return await self._async_invoke_aqara_cloud_api(
189 | intent="query.device.bindKey", did=did
190 | )
191 |
192 | async def async_query_device_info(
193 | self,
194 | dids: list = None,
195 | position_id: str = None,
196 | page_num: int = None,
197 | page_size: int = None,
198 | ):
199 | """查询设备信息"""
200 | resp = await self._async_invoke_aqara_cloud_api(
201 | intent="query.device.info",
202 | dids=dids,
203 | positionId=position_id,
204 | pageNum=page_num,
205 | pageSize=page_size,
206 | )
207 | if resp:
208 | return resp.get("data")
209 | return {}
210 |
211 | async def async_query_all_devices_info(self, page_size: int = 50):
212 | """查询所有设备信息"""
213 | continue_flag = True
214 | page_num = 1
215 | devices = []
216 | while continue_flag:
217 | jo = await self.async_query_device_info(
218 | page_num=page_num, page_size=page_size
219 | )
220 | devices.extend(jo)
221 | if len(jo) < page_size:
222 | continue_flag = False
223 | page_num = page_num + 1
224 | return devices
225 |
226 | async def async_query_device_sub_info(self, did: str):
227 | """查询网关下子设备信息"""
228 | return await self._async_invoke_aqara_cloud_api(
229 | intent="query.device.subInfo", did=did
230 | )
231 |
232 | async def async_query_resource_info(self, model: str, resource_id: str = None):
233 | """查询已开放的资源详情"""
234 | return await self._async_invoke_aqara_cloud_api(
235 | intent="query.resource.info", model=model, resourceId=resource_id
236 | )
237 |
238 | async def async_query_resource_value(self, subject_id: str, resource_ids: list):
239 | """查询资源信息"""
240 | return await self._async_invoke_aqara_cloud_api(
241 | intent="query.resource.value",
242 | resources=[{"subjectId": subject_id, "resourceIds": resource_ids}],
243 | )
244 |
245 | async def async_write_resource_device(
246 | self, subject_id: str, resource_id: str, value: str
247 | ):
248 | """控制设备"""
249 | return await self._async_invoke_aqara_cloud_api(
250 | intent="write.resource.device",
251 | list_data=True,
252 | subjectId=subject_id,
253 | resources=[{"resourceId": resource_id, "value": value}],
254 | )
255 |
256 | async def async_write_device_openconnect(
257 | self, subject_id: str
258 | ):
259 | """开启网关添加子设备模式"""
260 | return await self._async_invoke_aqara_cloud_api(
261 | intent="write.device.openConnect",
262 | resources=[{"subjectId": subject_id}]
263 | )
264 |
265 | async def async_write_device_closeconnect(
266 | self, subject_id: str
267 | ):
268 | """关闭网关添加子设备模式"""
269 | return await self._async_invoke_aqara_cloud_api(
270 | intent="write.device.closeConnect",
271 | resources=[{"subjectId": subject_id}]
272 | )
273 |
274 | async def async_subscribe_resources(
275 | self, subject_id: str, resource_ids: list, attach=None
276 | ):
277 | """订阅资源"""
278 | return await self._async_invoke_aqara_cloud_api(
279 | intent="config.resource.subscribe",
280 | resources=[{"subjectId": subject_id, "resourceIds": resource_ids, "attach": attach}],
281 | )
282 |
283 | async def async_unsubscribe_resources(
284 | self, subject_id: str, resource_ids: list, attach=None
285 | ):
286 | """取消订阅资源"""
287 | return await self._async_invoke_aqara_cloud_api(
288 | intent="config.resource.unsubscribe",
289 | resources=[{"subjectId": subject_id, "resourceIds": resource_ids, "attach": attach}],
290 | )
291 |
292 | async def async_write_ir_startlearn(
293 | self, subject_id: str, time_length=20
294 | ):
295 | """开启红外学习"""
296 | return await self._async_invoke_aqara_cloud_api(
297 | intent="write.ir.startLearn",
298 | resources=[{"subjectId": subject_id, "timeLength": time_length}]
299 | )
300 |
301 | async def async_write_ir_cancellearn(
302 | self, subject_id: str
303 | ):
304 | """取消开启红外学习"""
305 | return await self._async_invoke_aqara_cloud_api(
306 | intent="write.ir.cancelLearn",
307 | resources=[{"subjectId": subject_id}]
308 | )
309 |
310 | async def async_query_ir_learnresult(
311 | self, subject_id: str, keyid: str
312 | ):
313 | """查询红外学习结果"""
314 | return await self._async_invoke_aqara_cloud_api(
315 | intent="query.ir.learnResult",
316 | resources=[{"subjectId": subject_id, "keyId": keyid}]
317 | )
318 |
--------------------------------------------------------------------------------
/custom_components/aqara_bridge/core/aiot_manager.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import json
3 | import logging
4 | from typing import Optional, Union
5 |
6 | from homeassistant.core import HomeAssistant
7 | from homeassistant.config_entries import ConfigEntry
8 | from homeassistant.helpers.entity import DeviceInfo, Entity
9 | from rocketmq.client import PushConsumer, RecvMessage
10 |
11 | from .aiot_cloud import AiotCloud, APP_ID, KEY_ID, APP_KEY
12 | from .aiot_mapping import (
13 | MK_MAPPING_PARAMS,
14 | MK_INIT_PARAMS,
15 | MK_RESOURCES,
16 | MK_HASS_NAME,
17 | AIOT_DEVICE_MAPPING,
18 | )
19 | from .const import DOMAIN, HASS_DATA_AIOT_MANAGER, CONF_DEBUG
20 |
21 |
22 | _LOGGER = logging.getLogger(__name__)
23 |
24 |
25 | class AiotDevice:
26 | def __init__(self, **kwargs):
27 | self.did = kwargs.get("did")
28 | self.parent_did = kwargs.get("parentDid")
29 | self.model = kwargs.get("model")
30 | self.model_type = kwargs.get("modelType")
31 | self.device_name = kwargs.get("deviceName")
32 | self.state = kwargs.get("state")
33 | self.timezone = kwargs.get("timeZone")
34 | self.firmware_version = kwargs.get("firmwareVersion")
35 | self.create_time = kwargs.get("createTime")
36 | self.update_time = kwargs.get("updateTime")
37 | self.platforms = None
38 | for device in AIOT_DEVICE_MAPPING:
39 | if self.model in device:
40 | self.platforms = device['params']
41 | break
42 | self.children = []
43 |
44 | @property
45 | def is_supported(self):
46 | return self.platforms is not None
47 |
48 |
49 | class AiotEntityBase(Entity):
50 | _channel = None
51 | _device = None
52 | _res_params = None
53 | _supported_resources = None
54 | _aiot_manager = None
55 |
56 | _attr_lqi = None
57 | _attr_chip_temperature = None
58 | _attr_voltage = None
59 | _attr_fw_ver = None
60 |
61 | def __init__(self, hass, device, res_params, type_name, channel=None, **kwargs):
62 | self._device = device
63 | self._res_params = res_params
64 | self._supported_resources = []
65 | [
66 | self._supported_resources.append(v[0].format(channel))
67 | for k, v in res_params.items()
68 | ]
69 | self._channel = channel
70 |
71 | self.hass = hass
72 | self._attr_name = device.device_name
73 | self._attr_should_poll = False
74 | self._attr_unique_id = (
75 | f"{DOMAIN}.{type_name}_0x{device.did.split('.', 1)[1]}_{kwargs.get('hass_attr_name')}"
76 | )
77 | self.entity_id = f"{DOMAIN}.0x{device.did.split('.', 1)[1]}_{kwargs.get('hass_attr_name')}"
78 | if channel:
79 | self._attr_unique_id = f"{self._attr_unique_id}_{channel}"
80 | self.entity_id = f"{self.entity_id}_{channel}"
81 | self._attr_device_info = DeviceInfo(
82 | identifiers={(DOMAIN, device.did)},
83 | name=self._attr_name,
84 | model=device.model,
85 | manufacturer=(device.model or "Lumi").split(".", 1)[0].capitalize(),
86 | sw_version=device.firmware_version,
87 | )
88 | self._attr_supported_features = kwargs.get("supported_features")
89 | self._attr_unit_of_measurement = kwargs.get("unit_of_measurement")
90 | self._attr_device_class = kwargs.get("device_class")
91 |
92 | self._aiot_manager: AiotManager = hass.data[DOMAIN][HASS_DATA_AIOT_MANAGER]
93 |
94 | def debug(self, message: str):
95 | """ deubug function """
96 | self._aiot_manager.debug(f"{self.entity_id}: {message}")
97 |
98 | @property
99 | def channel(self) -> int:
100 | return self._channel
101 |
102 | @property
103 | def supported_resources(self) -> list:
104 | return self._supported_resources
105 |
106 | @property
107 | def device(self) -> AiotDevice:
108 | return self._device
109 |
110 | @property
111 | def lqi(self):
112 | """Return the signal strength of zigbee """
113 | return self._attr_lqi
114 |
115 | @property
116 | def chip_temperature(self):
117 | """Return the chip temperature."""
118 | return self._attr_chip_temperature
119 |
120 | @property
121 | def voltage(self):
122 | """Return battery voltage."""
123 | return self._attr_voltage
124 |
125 | @property
126 | def fw_ver(self):
127 | """Return firmware version."""
128 | return self._attr_fw_ver
129 |
130 | def get_res_id_by_name(self, res_name):
131 | return self._res_params[res_name][0].format(self._channel)
132 |
133 | async def async_set_res_value(self, res_name, value):
134 | """设置资源值"""
135 | res_id = self.get_res_id_by_name(res_name)
136 | if 'verbose' in self._aiot_manager.debug_option:
137 | self.debug("async_set_res_value {} {} {}".format(self.device.did, res_id, value))
138 | return await self._aiot_manager.session.async_write_resource_device(
139 | self.device.did, res_id, value
140 | )
141 |
142 | async def async_fetch_res_values(self, *args):
143 | """获取资源值"""
144 | res_ids = []
145 | if len(args) > 0:
146 | res_ids = args
147 | else:
148 | [
149 | res_ids.append(self.get_res_id_by_name(k))
150 | for k, v in self._res_params.items()
151 | ]
152 | return await self._aiot_manager.session.async_query_resource_value(
153 | self.device.did, res_ids
154 | )
155 |
156 | async def async_update(self):
157 | resp = await self.async_fetch_res_values()
158 | if resp:
159 | for x in resp:
160 | await self.async_set_attr(x["resourceId"], x["value"], write_ha_state=False)
161 |
162 | async def async_set_resource(self, res_name, attr_value):
163 | """设置aiot resource的值"""
164 | tup_res = self._res_params.get(res_name)
165 | if tup_res:
166 | res_value = attr_value
167 | current_value = getattr(self, tup_res[1])
168 | resp = None
169 | if 'verbose' in self._aiot_manager.debug_option:
170 | self.debug("set {} with value {} on {}".format(self.device.did, res_value, res_name))
171 | if current_value != attr_value:
172 | res_value = self.convert_attr_to_res(res_name, attr_value)
173 | resp = await self.async_set_res_value(res_name, res_value)
174 | # TODO 这里需要判断是否调用成功,再进行赋值
175 | self.__setattr__(tup_res[1], attr_value)
176 | self.async_write_ha_state()
177 | return resp
178 |
179 | async def async_set_attr(self, res_id, res_value, write_ha_state=True):
180 | """设置ha attr的值"""
181 | res_name = next(
182 | k
183 | for k, v in self._res_params.items()
184 | if v[0].format(self.channel) == res_id
185 | )
186 | tup_res = self._res_params.get(res_name)
187 | attr_value = self.convert_res_to_attr(res_name, res_value)
188 | current_value = getattr(self, tup_res[1], None)
189 | if 'verbose' in self._aiot_manager.debug_option:
190 | self.debug("set value {} current: {} write: {}".format(attr_value, current_value, write_ha_state))
191 | if current_value != attr_value:
192 | self.__setattr__(tup_res[1], attr_value)
193 | if write_ha_state:
194 | self.async_write_ha_state() # 初始化的时候不能执行这句话,会创建其他乱七八糟的对象
195 |
196 | async def async_device_connection(self, Open=False):
197 | """ enable/disable device connection """
198 | if 'verbose' in self._aiot_manager.debug_option:
199 | self.debug("async_device_connection {}".format(self.device.did))
200 | if Open:
201 | return await self._aiot_manager.session.async_write_device_openconnect(
202 | self.device.did
203 | )
204 | return await self._aiot_manager.session.async_write_device_closeconnect(
205 | self.device.did
206 | )
207 |
208 | async def async_infrared_learn(self, Enable=False, timelength=20):
209 | """ enable/disable infrared learn """
210 | if Enable:
211 | return await self._aiot_manager.session.async_write_ir_startlearn(
212 | self.device.did, time_length=timelength
213 | )
214 | return await self._aiot_manager.session.async_write_ir_cancellearn(
215 | self.device.did
216 | )
217 |
218 | async def async_received_learnresult(self, keyid):
219 | """ receive infrared learn result """
220 | return await self._aiot_manager.session.async_query_ir_learnresult(
221 | self.device.did, keyid
222 | )
223 |
224 | def convert_attr_to_res(self, res_name, attr_value):
225 | """从attr转换到res"""
226 | return attr_value
227 |
228 | def convert_res_to_attr(self, res_name, res_value):
229 | """从res转换到attr"""
230 | return res_value
231 |
232 |
233 | class AiotToggleableEntityBase(AiotEntityBase):
234 | def __init__(self, hass, device, res_params, type_name, channel, **kwargs):
235 | super().__init__(hass, device, res_params, type_name, channel=channel, **kwargs)
236 | self._attr_is_on = False
237 |
238 | async def async_turn_on(self, **kwargs):
239 | await self.async_set_resource("toggle", True)
240 |
241 | async def async_turn_off(self, **kwargs):
242 | await self.async_set_resource("toggle", False)
243 |
244 | def convert_attr_to_res(self, res_name, attr_value):
245 | if res_name == "toggle":
246 | # res_value:bool
247 | return "1" if attr_value else "0"
248 | return super().convert_attr_to_res(res_name, attr_value)
249 |
250 | def convert_res_to_attr(self, res_name, res_value):
251 | if res_name == "toggle":
252 | # res_value:0或1,字符串
253 | return res_value == "1"
254 | return super().convert_res_to_attr(res_name, res_value)
255 |
256 |
257 | class AiotMessageHandler:
258 | def __init__(self, loop):
259 | self._loop = loop
260 | self._consumer = PushConsumer(APP_ID)
261 | self._consumer.set_namesrv_addr("3rd-subscription.aqara.cn:9876")
262 | self._consumer.set_session_credentials(KEY_ID, APP_KEY, "")
263 |
264 | def start(self, callback):
265 | def consumer_callback(msg: RecvMessage):
266 | asyncio.set_event_loop(self._loop)
267 | asyncio.run_coroutine_threadsafe(
268 | callback(json.loads(str(msg.body, "utf-8"))),
269 | self._loop,
270 | )
271 |
272 | self._consumer.subscribe(APP_ID, consumer_callback)
273 | self._consumer.start()
274 |
275 | def stop(self):
276 | self._consumer.shutdown()
277 |
278 |
279 | class AiotManager:
280 | # Aiot会话
281 | _session: AiotCloud = None
282 |
283 | # 所有设备
284 | _all_devices: Optional[Union[str, AiotDevice]] = {}
285 |
286 | # 所有在HA中管理的设备
287 | _managed_devices: Optional[Union[str, AiotDevice]] = {}
288 |
289 | # 配置对象和设备的对应关系,1:N
290 | _entries_devices: Optional[Union[str, list]] = {}
291 |
292 | # 所有配置对象
293 | _config_entries: Optional[Union[str, ConfigEntry]] = {}
294 |
295 | # 设备和实体的对应关系,1:N
296 | _devices_entities: Optional[Union[str, list]] = {}
297 |
298 | # 插件不支持的设备列表
299 | _unsupported_devices: Optional[list] = []
300 |
301 | debug_option = ''
302 |
303 | def __init__(self, hass: HomeAssistant, session: AiotCloud):
304 | self._hass = hass
305 | self._session = session
306 | self._msg_handler = AiotMessageHandler(asyncio.get_event_loop())
307 | self._msg_handler.start(self._msg_callback)
308 | self._options = None
309 |
310 | def debug(self, message: str):
311 | """ deubug function """
312 | if self._options is None:
313 | self._options = self._session.get_options()
314 | self.debug_option = self._options.get('debug', '')
315 | if 'true' in self.debug_option:
316 | _LOGGER.error(f"{message}")
317 |
318 | @property
319 | def session(self) -> AiotCloud:
320 | """与Aiot建立的会话"""
321 | return self._session
322 |
323 | @property
324 | def all_devices(self) -> Optional[list]:
325 | """获取Aiot Cloud上的所有设备"""
326 | return self._all_devices.values()
327 |
328 | @property
329 | def unmanaged_gateways(self) -> Optional[list]:
330 | """获取HA为管理的网关设备"""
331 | gateways = []
332 | [
333 | gateways.append(x)
334 | for x in self._all_devices.values()
335 | if x.model_type in (1, 2) and x.did not in self._managed_devices.keys()
336 | ]
337 | return gateways
338 |
339 | @property
340 | def unsupported_devices(self) -> Optional[list]:
341 | """插件不支持的设备列表"""
342 | devices = []
343 | [devices.append(x) for x in self._all_devices.values() if not x.is_supported]
344 | return devices
345 |
346 | async def _msg_callback(self, msg):
347 | """消息推送格式,见https://opendoc.aqara.cn/docs/%E4%BA%91%E5%AF%B9%E6%8E%A5%E5%BC%80%E5%8F%91%E6%89%8B%E5%86%8C/%E6%B6%88%E6%81%AF%E6%8E%A8%E9%80%81/%E6%B6%88%E6%81%AF%E6%8E%A8%E9%80%81%E6%A0%BC%E5%BC%8F.html"""
348 | if msg.get("msgType"):
349 | # 属性消息,resource_report
350 | if 'msg' in self.debug_option:
351 | self.debug("msgType {}".format(msg['data']))
352 | for x in msg["data"]:
353 | entities = self._devices_entities.get(x["subjectId"])
354 | if entities:
355 | for entity in entities:
356 | if x["resourceId"] in entity.supported_resources:
357 | await entity.async_set_attr(x["resourceId"], x["value"])
358 | elif msg.get("eventType"):
359 | if 'msg' in self.debug_option:
360 | self.debug("eventType {}".format(msg['data']))
361 | # 事件消息
362 | if msg["eventType"] == "gateway_bind": # 网关绑定
363 | pass
364 | elif msg["eventType"] == "subdevice_bind" or msg['cause'] == 11: # 子设备绑定
365 | pass
366 | elif msg["eventType"] == "gateway_unbind": # 网关解绑
367 | pass
368 | elif msg["eventType"] == "unbind_sub_gw": # 子设备解绑
369 | pass
370 | elif msg["eventType"] == "gateway_online": # 网关在线
371 | pass
372 | elif msg["eventType"] == "gateway_offline": # 网关离线
373 | pass
374 | elif msg["eventType"] == "subdevice_online": # 子设备在线
375 | pass
376 | elif msg["eventType"] == "subdevice_offline": # 子设备离线
377 | pass
378 | else: # 其他事件暂不处理
379 | pass
380 | else:
381 | _LOGGER.warn("unknown msg: {}".format(msg))
382 |
383 | async def async_refresh_all_devices(self):
384 | """获取Aiot所有设备"""
385 | self._all_devices = {}
386 | results = await self._session.async_query_all_devices_info()
387 | [self._all_devices.setdefault(x["did"], AiotDevice(**x)) for x in results]
388 |
389 | async def async_add_devices(
390 | self,
391 | config_entry: ConfigEntry,
392 | devices: Optional[list],
393 | auto_add_sub_devices=False,
394 | ):
395 | await self.async_refresh_all_devices() # 刷新一次所有设备列表
396 | self._entries_devices.setdefault(config_entry.entry_id, [])
397 | self._config_entries[config_entry.entry_id] = config_entry
398 | for device in devices:
399 | # 这里看情况检查did是否已经存在,理论上来说应该不会重复,现在代码未做重复判断
400 | if device.is_supported:
401 | self._managed_devices[device.did] = device
402 | self._entries_devices[config_entry.entry_id].append(device.did)
403 | if auto_add_sub_devices and device.model_type == 1:
404 | sub_devices = []
405 | [
406 | sub_devices.append(x)
407 | for x in self.all_devices
408 | if x.parent_did == device.did
409 | ]
410 | for sub_device in sub_devices:
411 | if sub_device.is_supported:
412 | device.children.append(sub_device)
413 | self._managed_devices[sub_device.did] = sub_device
414 | self._entries_devices[config_entry.entry_id].append(
415 | sub_device.did
416 | )
417 | else:
418 | _LOGGER.warn(
419 | f"Aqara device is not supported. Deivce model is '{sub_device.model}'."
420 | )
421 | else:
422 | _LOGGER.warn(
423 | f"Aqara device is not supported. Deivce model is '{device.model}'."
424 | )
425 | continue
426 |
427 | async def async_forward_entry_setup(self, config_entry: ConfigEntry):
428 | devices_in_entry = self._entries_devices[config_entry.entry_id]
429 | platforms = []
430 | for x in devices_in_entry:
431 | if self._managed_devices[x].is_supported:
432 | for i in range(len(self._managed_devices[x].platforms)):
433 | platforms.extend(self._managed_devices[x].platforms[i].keys())
434 |
435 | platforms = set(platforms)
436 | [
437 | self._hass.async_create_task(
438 | self._hass.config_entries.async_forward_entry_setup(config_entry, x)
439 | )
440 | for x in platforms
441 | ]
442 |
443 | async def async_add_entities(
444 | self, config_entry: ConfigEntry, entity_type: str, cls_list, async_add_entities
445 | ):
446 | """根据ConfigEntry创建Entity"""
447 | devices = []
448 | for x in self._entries_devices[config_entry.entry_id]:
449 | for i in range(len(self._managed_devices[x].platforms)):
450 | # if any one entity_type exist, append device
451 | if entity_type in self._managed_devices[x].platforms[i]:
452 | devices.append(self._managed_devices[x])
453 | break
454 |
455 | entities = []
456 | for device in devices:
457 | params = []
458 | self._devices_entities.setdefault(device.did, [])
459 | for aiot_device in AIOT_DEVICE_MAPPING:
460 | if device.model in aiot_device:
461 | for p in aiot_device['params']:
462 | if entity_type in p:
463 | params.append(p[entity_type])
464 | break
465 |
466 | ch_count = None
467 | # 这里需要处理特殊设备
468 | if device.model == "lumi.airrtc.vrfegl01":
469 | # VRF空调控制器
470 | resp = await self._session.async_query_resource_value(
471 | device.did, ["13.1.85"]
472 | )
473 | ch_count = int(resp[0]["value"])
474 |
475 | for j in range(len(params)):
476 | if params[j].get(MK_MAPPING_PARAMS):
477 | ch_count = ch_count or params[j][MK_MAPPING_PARAMS].get("ch_count")
478 |
479 | if ch_count:
480 | for i in range(ch_count):
481 | for j in range(len(params)):
482 | attr = params[j].get(MK_INIT_PARAMS)[MK_HASS_NAME]
483 | t = cls_list.get(attr, None)
484 | if t is None:
485 | t = cls_list['default']
486 | instance = t(
487 | self._hass,
488 | device,
489 | params[j][MK_RESOURCES],
490 | i + 1,
491 | **params[j].get(MK_INIT_PARAMS) or {},
492 | )
493 | self._devices_entities[device.did].append(instance)
494 | entities.append(instance)
495 | else:
496 | for i in range(len(params)):
497 | attr = params[i].get(MK_INIT_PARAMS)[MK_HASS_NAME]
498 | t = cls_list.get(attr, None)
499 | if t is None:
500 | t = cls_list['default']
501 | instance = t(
502 | self._hass,
503 | device,
504 | params[i][MK_RESOURCES],
505 | **params[i].get(MK_INIT_PARAMS) or {},
506 | )
507 | self._devices_entities[device.did].append(instance)
508 | entities.append(instance)
509 | async_add_entities(entities, update_before_add=True)
510 |
511 | async def async_remove_entry(self, config_entry):
512 | """ConfigEntry remove."""
513 | self._config_entries.pop(config_entry.entry_id)
514 | device_ids = self._entries_devices[config_entry.entry_id]
515 | for device_id in device_ids:
516 | self._managed_devices.pop(device_id)
517 | self._devices_entities.pop(device_id)
518 |
--------------------------------------------------------------------------------
/custom_components/aqara_bridge/core/aiot_mapping.py:
--------------------------------------------------------------------------------
1 | from homeassistant.components.climate import TEMP_CELSIUS
2 | from homeassistant.components.light import (
3 | SUPPORT_BRIGHTNESS,
4 | SUPPORT_COLOR,
5 | SUPPORT_COLOR_TEMP,
6 | )
7 | from homeassistant.components.cover import (
8 | SUPPORT_CLOSE,
9 | SUPPORT_OPEN,
10 | SUPPORT_SET_POSITION,
11 | SUPPORT_STOP,
12 | )
13 | from homeassistant.components.climate import (
14 | SUPPORT_TARGET_TEMPERATURE,
15 | SUPPORT_FAN_MODE,
16 | )
17 | from homeassistant.components.remote import SUPPORT_LEARN_COMMAND
18 | from homeassistant.components.binary_sensor import (
19 | DEVICE_CLASS_DOOR,
20 | DEVICE_CLASS_MOISTURE,
21 | DEVICE_CLASS_MOTION
22 | )
23 | from homeassistant.const import (
24 | # ATTR_BATTERY_LEVEL,
25 | # ATTR_TEMPERATURE,
26 | CONDUCTIVITY,
27 | DEVICE_CLASS_BATTERY,
28 | DEVICE_CLASS_ENERGY,
29 | DEVICE_CLASS_HUMIDITY,
30 | DEVICE_CLASS_ILLUMINANCE,
31 | DEVICE_CLASS_POWER,
32 | DEVICE_CLASS_PRESSURE,
33 | DEVICE_CLASS_TEMPERATURE,
34 | DEVICE_CLASS_CO2,
35 | ENERGY_WATT_HOUR,
36 | ENERGY_KILO_WATT_HOUR,
37 | LIGHT_LUX,
38 | PERCENTAGE,
39 | POWER_WATT,
40 | PRESSURE_HPA,
41 | TEMP_CELSIUS,
42 | CONCENTRATION_PARTS_PER_BILLION,
43 | CONCENTRATION_PARTS_PER_MILLION,
44 | STATE_OPEN,
45 | STATE_OPENING,
46 | # STATE_CLOSED,
47 | STATE_CLOSING,
48 | STATE_LOCKED,
49 | STATE_UNLOCKED
50 | )
51 |
52 | try:
53 | from homeassistant.const import DEVICE_CLASS_GAS
54 | except:
55 | DEVICE_CLASS_GAS = "gas"
56 | try:
57 | from homeassistant.const import DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS
58 | except:
59 | DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS = "volatile_organic_compounds"
60 |
61 | # AiotDevice Mapping
62 | MK_MAPPING_PARAMS = "mapping_params"
63 | MK_INIT_PARAMS = "init_params"
64 | MK_RESOURCES = "resources"
65 | MK_HASS_NAME = "hass_attr_name"
66 |
67 | AIOT_DEVICE_MAPPING = [{
68 | # Aqara M1S网关
69 | 'lumi.gateway.acn01': ["Aqara", "Gateway M1S", "ZHWG15LM"],
70 | 'params': [
71 | {
72 | "remote": {
73 | MK_INIT_PARAMS: {
74 | MK_HASS_NAME: "pair",
75 | "supported_features": 0
76 | },
77 | MK_RESOURCES: {
78 | "pair": ("8.0.2109", "_attr_is_on"),
79 | }
80 | }
81 | }, {
82 | "light": {
83 | MK_INIT_PARAMS: {
84 | MK_HASS_NAME: "light",
85 | "supported_features": SUPPORT_BRIGHTNESS | SUPPORT_COLOR,
86 | "color_mode": "hs",
87 | },
88 | MK_RESOURCES: {
89 | "toggle": ("14.7.111", "_attr_is_on"),
90 | "color": ("14.7.85", "_attr_hs_color"),
91 | "brightness": ("14.7.1006", "_attr_brightness"),
92 | }
93 | }
94 | }, {
95 | "sensor": {
96 | MK_INIT_PARAMS: {
97 | MK_HASS_NAME: "illuminance",
98 | "device_class": DEVICE_CLASS_ILLUMINANCE,
99 | "state_class": "measurement",
100 | "unit_of_measurement": LIGHT_LUX
101 | },
102 | MK_RESOURCES: {"illumination": ("0.3.85", "_attr_native_value")},
103 | }
104 | }
105 | ]
106 | },{
107 | 'lumi.aircondition.acn05': ["Aqara", "AirCondition P3", "KTBL12LM"],
108 | 'params': [
109 | {
110 | "remote": {
111 | MK_INIT_PARAMS: {
112 | MK_HASS_NAME: "pair",
113 | "supported_features": 0
114 | },
115 | MK_RESOURCES: {
116 | "pair": ("8.0.2109", "_attr_is_on"),
117 | }
118 | }
119 | }, {
120 | "remote": {
121 | MK_INIT_PARAMS: {
122 | MK_HASS_NAME: "ir",
123 | "supported_features": SUPPORT_LEARN_COMMAND
124 | },
125 | MK_RESOURCES: {
126 | "irda": ("8.0.2092", "_attr_is_on"),
127 | }
128 | }
129 | }
130 | ]
131 | }, {
132 | 'lumi.gateway.aqcn02': ["Aqara", "Hub E1", "ZHWG16LM"],
133 | 'params': [
134 | {
135 | "remote": {
136 | MK_INIT_PARAMS: {
137 | MK_HASS_NAME: "pair",
138 | "supported_features": 0
139 | },
140 | MK_RESOURCES: {
141 | "pair": ("8.0.2109", "_attr_is_on"),
142 | }
143 | }
144 | }
145 | ]
146 | }, {
147 | 'lumi.gateway.iragl5': ["Aqara", "Gateway M2", "ZHWG12LM"],
148 | 'params': [
149 | {
150 | "remote": {
151 | MK_INIT_PARAMS: {
152 | MK_HASS_NAME: "pair",
153 | "supported_features": 0
154 | },
155 | MK_RESOURCES: {
156 | "pair": ("8.0.2109", "_attr_is_on"),
157 | }
158 | }
159 | }, {
160 | "remote": {
161 | MK_INIT_PARAMS: {
162 | MK_HASS_NAME: "ir",
163 | "supported_features": SUPPORT_LEARN_COMMAND
164 | },
165 | MK_RESOURCES: {
166 | "irda": ("8.0.2092", "_attr_is_on"),
167 | }
168 | }
169 | }
170 | ]
171 | }, {
172 | 'lumi.gateway.sacn01': ["Aqara", "Smart Hub H1", "QBCZWG11LM"],
173 | 'params': [
174 | {
175 | "remote": {
176 | MK_INIT_PARAMS: {
177 | MK_HASS_NAME: "pair",
178 | "supported_features": 0
179 | },
180 | MK_RESOURCES: {
181 | "pair": ("8.0.2109", "_attr_is_on"),
182 | }
183 | }
184 | }
185 | ]
186 | }, {
187 | 'lumi.camera.gwagl02': ["Aqara", "Camera Hub G2H", "ZNSXJ12LM"],
188 | 'params': [
189 | {
190 | "remote": {
191 | MK_INIT_PARAMS: {
192 | MK_HASS_NAME: "pair",
193 | "supported_features": 0
194 | },
195 | MK_RESOURCES: {
196 | "pair": ("8.0.2109", "_attr_is_on"),
197 | }
198 | }
199 | }
200 | ]
201 | }, {
202 | 'lumi.camera.gwpagl01': ["Aqara", "Camera Hub G3", "ZNSXJ13LM"],
203 | 'params': [
204 | {
205 | "remote": {
206 | MK_INIT_PARAMS: {
207 | MK_HASS_NAME: "pair",
208 | "supported_features": 0
209 | },
210 | MK_RESOURCES: {
211 | "pair": ("8.0.2109", "_attr_is_on"),
212 | }
213 | }
214 | }
215 | ]
216 | }, {
217 | 'lumi.gateway.acn004': ["Aqara", "Gateway M1S22", "ZHWG20LM"],
218 | 'params': [
219 | {
220 | "remote": {
221 | MK_INIT_PARAMS: {
222 | MK_HASS_NAME: "pair",
223 | "supported_features": 0
224 | },
225 | MK_RESOURCES: {
226 | "pair": ("8.0.2109", "_attr_is_on"),
227 | }
228 | }
229 | }, {
230 | "light": {
231 | MK_INIT_PARAMS: {
232 | MK_HASS_NAME: "light",
233 | "supported_features": SUPPORT_BRIGHTNESS | SUPPORT_COLOR,
234 | "color_mode": "hs",
235 | },
236 | MK_RESOURCES: {
237 | "toggle": ("14.7.111", "_attr_is_on"),
238 | "color": ("14.7.85", "_attr_hs_color"),
239 | "brightness": ("14.7.1006", "_attr_brightness"),
240 | }
241 | }
242 | }, {
243 | "sensor": {
244 | MK_INIT_PARAMS: {
245 | MK_HASS_NAME: "illuminance",
246 | "device_class": DEVICE_CLASS_ILLUMINANCE,
247 | "state_class": "measurement",
248 | "unit_of_measurement": LIGHT_LUX
249 | },
250 | MK_RESOURCES: {"illumination": ("0.3.85", "_attr_native_value")},
251 | }
252 | }
253 | ]
254 | }, {
255 | 'virtual.ir.default': ["Aqara", "Virtual IR", ""],
256 | 'params': [
257 | {
258 | "remote": {
259 | MK_INIT_PARAMS: {
260 | MK_HASS_NAME: "ir",
261 | "supported_features": 0
262 | },
263 | MK_RESOURCES: {
264 | "irda": ("8.0.2092", "_attr_is_on"),
265 | }
266 | }
267 | }
268 | ]
269 | }, {
270 | 'lumi.ctrl_neutral1': ["Aqara", "Single Wall Switch", "QBKG04LM"],
271 | 'lumi.switch.b1lacn02': ["Aqara", "Single Wall Switch D1", "QBKG21LM"],
272 | 'lumi.switch.b1lc04': ["Aqara", "Single Wall Switch E1", "QBKG38LM"],
273 | 'lumi.switch.b1laus01': ["Aqara", "Single Wall Switch US", "WS-USC01"],
274 | 'lumi.switch.l1aeu1': ["Aqara", "Single Wall Switch EU H1", "WS-EUK01"],
275 | 'lumi.switch.l0agl1': ["Aqara", "Relay T1", "SSM-U02"],
276 | 'lumi.switch.l0acn1': ["Aqara", "Relay T1", "DLKZMK12LM"],
277 | # 智能墙壁开关T1(单火单键)
278 | 'lumi.switch.b1lacn01': ["Aqara", "Single Wall Switch T1", "QBKG17LM"],
279 | 'lumi.switch.acn001': ["Aqara", "Single Wall Switch X1", ""],
280 | 'lumi.switch.b1nc01': ["Aqara", "Single Wall Switch E1", "QBKG40LM"],
281 | 'params': [
282 | {
283 | "switch": {
284 | MK_INIT_PARAMS: {
285 | MK_HASS_NAME: "switch",
286 | },
287 | MK_RESOURCES: {
288 | "toggle": ("4.1.85", "_attr_is_on"),
289 | "power": ("0.12.85", "_attr_current_power_w"),
290 | "energy": ("0.13.85", "_attr_today_energy_kwh"),
291 | "chip_temperature": ("8.0.2006", "_attr_chip_temperature"),
292 | "lqi": ("8.0.2007", "_attr_lqi")
293 | }
294 | }
295 | }
296 | ]
297 | }, {
298 | 'lumi.ctrl_neutral2': ["Aqara", "Double Wall Switch", "QBKG03LM"],
299 | 'lumi.switch.b2lacn02': ["Aqara", "Double Wall Switch D1", "QBKG22LM"],
300 | 'lumi.switch.b2lc04': ["Aqara", "Double Wall Switch E1", "QBKG39LM"],
301 | 'lumi.switch.b2laus01': ["Aqara", "Double Wall Switch US", "WS-USC02"],
302 | 'lumi.switch.l2aeu1': ["Aqara", "Double Wall Switch EU H1", "WS-EUK02"],
303 | # 智能墙壁开关T1(单火双键)
304 | 'lumi.switch.b2lacn01': ["Aqara", "Double Wall Switch T1", "QBKG18LM"],
305 | 'lumi.switch.acn002': ["Aqara", "Double Wall Switch X1", ""],
306 | 'lumi.switch.b2nc01': ["Aqara", "Double Wall Switch E1", "QBKG41LM"],
307 | 'params': [
308 | {
309 | "switch": {
310 | MK_INIT_PARAMS: {
311 | MK_HASS_NAME: "switch",
312 | },
313 | MK_RESOURCES: {
314 | "toggle": ("4.{}.85", "_attr_is_on"),
315 | "power": ("0.12.85", "_attr_current_power_w"),
316 | "energy": ("0.13.85", "_attr_today_energy_kwh"),
317 | "chip_temperature": ("8.0.2006", "_attr_chip_temperature"),
318 | "lqi": ("8.0.2007", "_attr_lqi")
319 | }
320 | }
321 | }
322 | ]
323 | }, {
324 | 'lumi.switch.l3acn3': ["Aqara", "Triple Wall Switch D1", "QBKG25LM"],
325 | # 智能墙壁开关T1(单火三键)
326 | 'lumi.switch.acn003': ["Aqara", "Triple Wall Switch X1", ""],
327 | 'lumi.switch.b3l01': ["Aqara", "Triple Wall Switch T1", "QBKG33LM"],
328 | 'params': [
329 | {
330 | "switch": {
331 | MK_INIT_PARAMS: {
332 | MK_HASS_NAME: "switch",
333 | },
334 | MK_RESOURCES: {
335 | "toggle": ("4.{}.85", "_attr_is_on"),
336 | "power": ("0.12.85", "_attr_current_power_w"),
337 | "energy": ("0.13.85", "_attr_today_energy_kwh"),
338 | "chip_temperature": ("8.0.2006", "_attr_chip_temperature"),
339 | "lqi": ("8.0.2007", "_attr_lqi")
340 | }
341 | }
342 | }
343 | ]
344 | }, {
345 | 'lumi.ctrl_ln1': ["Aqara", "Single Wall Switch", "QBKG11LM"],
346 | 'lumi.ctrl_ln1.aq1': ["Aqara", "Single Wall Switch", "QBKG11LM"],
347 | 'lumi.ctrl_86plug.v1': ["Aqara", "Socket", "QBCZ11LM"],
348 | 'lumi.ctrl_86plug.aq1': ["Aqara", "Socket", "QBCZ11LM"],
349 | 'lumi.ctrl_86plug.es1': ["Aqara", "Socket", "QBCZ11LM"],
350 | 'lumi.switch.b1nacn02': ["Aqara", "Single Wall Switch D1", "QBKG23LM"],
351 | 'lumi.switch.n1acn1': ["Aqara", "Single Wall Switch H1 Pro", "QBKG30LM"],
352 | 'lumi.switch.b1naus01': ["Aqara", "Single Wall Switch US", "WS-USC03"],
353 | 'lumi.switch.n1aeu1': ["Aqara", "Single Wall Switch EU H1", "WS-EUK03"],
354 | 'lumi.switch.n0agl1': ["Aqara", "Relay T1", "SSM-U01"],
355 | 'lumi.switch.n0acn1': ["Aqara", "Relay T1", "DLKZMK11LM"],
356 | 'lumi.switch.n0acn2': ["Aqara", "Relay T1", "DLKZMK11LM"],
357 | 'lumi.plug.maeu01': ["Aqara", "Plug", "SP-EUC01"],
358 | # 智能墙壁开关T1(零火单键)
359 | 'lumi.switch.b1nacn01': ["Aqara", "Single Wall Switch T1", "QBKG19LM"],
360 | 'lumi.switch.acn004': ["Aqara", "Single Wall Switch X1", ""],
361 | 'params': [
362 | {
363 | "switch": {
364 | MK_INIT_PARAMS: {
365 | MK_HASS_NAME: "switch",
366 | },
367 | MK_RESOURCES: {
368 | "toggle": ("4.1.85", "_attr_is_on"),
369 | "power": ("0.12.85", "_attr_current_power_w"),
370 | "energy": ("0.13.85", "_attr_today_energy_kwh"),
371 | "chip_temperature": ("8.0.2006", "_attr_chip_temperature"),
372 | "lqi": ("8.0.2007", "_attr_lqi")
373 | }
374 | }
375 | }, {
376 | "switch": {
377 | MK_INIT_PARAMS: {
378 | MK_HASS_NAME: "decoupled"
379 | },
380 | MK_RESOURCES: {
381 | "decoupled": ("4.11.85", "_attr_is_on")
382 | }
383 | }
384 | }, {
385 | "sensor": {
386 | MK_INIT_PARAMS: {
387 | MK_HASS_NAME: "power",
388 | "device_class": DEVICE_CLASS_POWER,
389 | "state_class": "measurement",
390 | "unit_of_measurement": POWER_WATT},
391 | MK_RESOURCES: {"power": ("0.12.85", "_attr_native_value")}
392 | }
393 | }, {
394 | "sensor": {
395 | MK_INIT_PARAMS: {
396 | MK_HASS_NAME: "energy",
397 | "device_class": DEVICE_CLASS_ENERGY,
398 | "state_class": "total_increasing",
399 | "unit_of_measurement": ENERGY_KILO_WATT_HOUR},
400 | MK_RESOURCES: {"energy": ("0.13.85", "_attr_native_value")},
401 | }
402 | }
403 | ]
404 | }, {
405 | 'lumi.relay.c2acn01': ["Aqara", "Relay", "LLKZMK11LM"],
406 | 'lumi.ctrl_ln2': ["Aqara", "Double Wall Switch", "QBKG12LM"],
407 | 'lumi.ctrl_ln2.aq1': ["Aqara", "Double Wall Switch", "QBKG12LM"],
408 | 'lumi.switch.b2nacn02': ["Aqara", "Double Wall Switch D1", "QBKG24LM"],
409 | 'lumi.switch.n2acn1': ["Aqara", "Double Wall Switch H1 Pro", "QBKG31LM"],
410 | 'lumi.switch.b2naus01': ["Aqara", "Double Wall Switch US", "WS-USC04"],
411 | 'lumi.switch.n2aeu1': ["Aqara", "Double Wall Switch EU H1", "WS-EUK04"],
412 | # 智能墙壁开关T1(零火双键)
413 | 'lumi.switch.b2nacn01': ["Aqara", "Double Wall Switch T1", "QBKG20LM"],
414 | 'lumi.switch.acn005': ["Aqara", "Double Wall Switch X1", ""],
415 | 'params': [
416 | {
417 | "switch": {
418 | MK_INIT_PARAMS: {
419 | MK_HASS_NAME: "switch",
420 | },
421 | MK_RESOURCES: {
422 | "toggle": ("4.{}.85", "_attr_is_on"),
423 | "power": ("0.12.85", "_attr_current_power_w"),
424 | "energy": ("0.13.85", "_attr_today_energy_kwh"),
425 | "chip_temperature": ("8.0.2006", "_attr_chip_temperature"),
426 | "lqi": ("8.0.2007", "_attr_lqi")
427 | }
428 | }
429 | }, {
430 | "switch": {
431 | MK_INIT_PARAMS: {
432 | MK_HASS_NAME: "decoupled"
433 | },
434 | MK_RESOURCES: {
435 | "decoupled": ("4.1{}.85", "_attr_is_on")
436 | }
437 | }
438 | }, {
439 | "sensor": {
440 | MK_INIT_PARAMS: {
441 | MK_HASS_NAME: "power",
442 | "device_class": DEVICE_CLASS_POWER,
443 | "state_class": "measurement",
444 | "unit_of_measurement": POWER_WATT},
445 | MK_RESOURCES: {"power": ("0.12.85", "_attr_native_value")}
446 | }
447 | }, {
448 | "sensor": {
449 | MK_INIT_PARAMS: {
450 | MK_HASS_NAME: "energy",
451 | "device_class": DEVICE_CLASS_ENERGY,
452 | "state_class": "total_increasing",
453 | "unit_of_measurement": ENERGY_KILO_WATT_HOUR},
454 | MK_RESOURCES: {"energy": ("0.13.85", "_attr_native_value")},
455 | }
456 | }
457 | ]
458 | }, {
459 | 'lumi.switch.n3acn3': ["Aqara", "Triple Wall Switch D1", "QBKG26LM"],
460 | 'lumi.switch.n3acn1': ["Aqara", "Triple Wall Switch H1 Pro", "QBKG32LM"],
461 | 'lumi.switch.n4acn4': ["Aqara", "Scene Panel", "ZNCJMB14LM"],
462 | # 智能墙壁开关T1(零火三键)
463 | 'lumi.switch.b3n01': ["Aqara", "Triple Wall Switch T1", "QBKG34LM"],
464 | 'lumi.switch.acn006': ["Aqara", "Triple Wall Switch X1", ""],
465 | 'params': [
466 | {
467 | "switch": {
468 | MK_INIT_PARAMS: {
469 | MK_HASS_NAME: "switch",
470 | },
471 | MK_RESOURCES: {
472 | "toggle": ("4.{}.85", "_attr_is_on"),
473 | "power": ("0.12.85", "_attr_current_power_w"),
474 | "energy": ("0.13.85", "_attr_today_energy_kwh"),
475 | "chip_temperature": ("8.0.2006", "_attr_chip_temperature"),
476 | "lqi": ("8.0.2007", "_attr_lqi")
477 | }
478 | }
479 | }, {
480 | "switch": {
481 | MK_INIT_PARAMS: {
482 | MK_HASS_NAME: "decoupled"
483 | },
484 | MK_RESOURCES: {
485 | "decoupled": ("4.1{}.85", "_attr_is_on")
486 | }
487 | }
488 | }, {
489 | "sensor": {
490 | MK_INIT_PARAMS: {
491 | MK_HASS_NAME: "power",
492 | "device_class": DEVICE_CLASS_POWER,
493 | "state_class": "measurement",
494 | "unit_of_measurement": POWER_WATT},
495 | MK_RESOURCES: {"power": ("0.12.85", "_attr_native_value")}
496 | }
497 | }, {
498 | "sensor": {
499 | MK_INIT_PARAMS: {
500 | MK_HASS_NAME: "energy",
501 | "device_class": DEVICE_CLASS_ENERGY,
502 | "state_class": "total_increasing",
503 | "unit_of_measurement": ENERGY_KILO_WATT_HOUR},
504 | MK_RESOURCES: {"energy": ("0.13.85", "_attr_native_value")},
505 | }
506 | }
507 | ]
508 | }, {
509 | # light with brightness and color temp
510 | 'lumi.light.aqcn02': ["Aqara", "Bulb", "ZNLDP12LM"],
511 | 'lumi.light.cwopcn02': ["Aqara", "Opple MX650", "XDD12LM"],
512 | 'lumi.light.cwopcn03': ["Aqara", "Opple MX480", "XDD13LM"],
513 | 'ikea.light.led1545g12': ["IKEA", "Bulb E27 980 lm", "LED1545G12"],
514 | 'ikea.light.led1546g12': ["IKEA", "Bulb E27 950 lm", "LED1546G12"],
515 | 'ikea.light.led1536g5': ["IKEA", "Bulb E14 400 lm", "LED1536G5"],
516 | 'ikea.light.led1537r6': ["IKEA", "Bulb GU10 400 lm", "LED1537R6"],
517 | 'lumi.light.cwacn1': ["Aqara", "0-10V Dimmer", "ZNTGMK12LM"],
518 | 'lumi.light.cwjwcn01': ["Aqara", "Jiawen 0-12V Dimmer", "Z204"],
519 | 'params': [
520 | {
521 | "light": {
522 | MK_INIT_PARAMS: {
523 | MK_HASS_NAME: "light",
524 | "supported_features": SUPPORT_BRIGHTNESS | SUPPORT_COLOR,
525 | "color_mode": "hs",
526 | },
527 | MK_RESOURCES: {
528 | "toggle": ("4.1.85", "_attr_is_on"),
529 | "brightness": ("14.1.85", "_attr_brightness"),
530 | "color_temp": ("14.2.85", "_attr_color_temp")
531 | }
532 | }
533 | }
534 | ]
535 | }, {
536 | # light with brightness and color temp
537 | 'lumi.light.cwac02': ["Aqara", "Bulb T1", "ZNLDP13LM"],
538 | 'params': [
539 | {
540 | "light": {
541 | MK_INIT_PARAMS: {
542 | MK_HASS_NAME: "light",
543 | "supported_features": SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP,
544 | "color_mode": "hs",
545 | },
546 | MK_RESOURCES: {
547 | "toggle": ("4.1.85", "_attr_is_on"),
548 | "brightness": ("1.7.85", "_attr_brightness"),
549 | "color_temp": ("1.9.85", "_attr_color_temp")
550 | }
551 | }
552 | }
553 | ]
554 | }, {
555 | 'lumi.light.rgbac1': ["Aqara", "RGBW LED Controller T1", "ZNTGMK11LM"],
556 | 'params': [
557 | {
558 | "light": {
559 | MK_INIT_PARAMS: {
560 | MK_HASS_NAME: "light",
561 | "supported_features": SUPPORT_BRIGHTNESS | SUPPORT_COLOR,
562 | "color_mode": "hs",
563 | },
564 | MK_RESOURCES: {
565 | "toggle": ("4.1.85", "_attr_is_on"),
566 | "brightness": ("14.1.85", "_attr_brightness"),
567 | "color_temp": ("14.2.85", "_attr_color_temp"),
568 | "rgb_color": ("14.8.85", "_attr_rgb_color")
569 | }
570 | }
571 | }
572 | ]
573 | }, {
574 | 'lumi.dimmer.rcbac1': ["Aqara", "RGBW LED Dimmer", "ZNDDMK11LM"],
575 | 'params': [
576 | {
577 | "light": {
578 | MK_INIT_PARAMS: {
579 | MK_HASS_NAME: "light",
580 | "supported_features": SUPPORT_BRIGHTNESS | SUPPORT_COLOR,
581 | "color_mode": "hs",
582 | },
583 | MK_RESOURCES: {
584 | "toggle": ("4.1.85", "_attr_is_on"),
585 | "brightness": ("14.1.85", "_attr_brightness"),
586 | "color_temp": ("14.2.85", "_attr_color_temp"),
587 | "rgb_color": ("14.8.85", "_attr_rgb_color"),
588 | "color": ("14.11.85", "_attr_hs_color")
589 | }
590 | }
591 | }
592 | ]
593 | }, {
594 | # VRF空调控制器
595 | 'lumi.airrtc.vrfegl01': ["Aqara", "VRF Air Conditioning", ""],
596 | 'params': [
597 | {
598 | "climate": {
599 | MK_INIT_PARAMS: {
600 | MK_HASS_NAME: "climate",
601 | "supported_features": SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE,
602 | "hvac_modes": ["cool", "heat", "off"],
603 | "unit_of_measurement": TEMP_CELSIUS,
604 | "target_temp_step": 1,
605 | "fan_modes": ["low", "middle", "high"],
606 | "min_temp": 16,
607 | "max_temp": 30,
608 | },
609 | MK_RESOURCES: {"ac_state": ("14.{}.85", "_state_str")},
610 | }
611 | }
612 | ]
613 | }, {
614 | 'lumi.curtain.acn002': ["Aqara", "Roller Shade E1", "ZNJLBL01LM"],
615 | # Aqara智能窗帘电机(锂电池开合帘版)
616 | 'lumi.curtain.hagl04': ["Aqara", "Curtain B1", "ZNCLDJ12LM"],
617 | 'params': [
618 | {
619 | "cover": {
620 | MK_INIT_PARAMS: {
621 | MK_HASS_NAME: "cover",
622 | "supported_features": SUPPORT_OPEN
623 | | SUPPORT_CLOSE
624 | | SUPPORT_SET_POSITION
625 | | SUPPORT_STOP,
626 | "device_class": "curtain",
627 | },
628 | MK_RESOURCES: {
629 | "curtain_state": ("14.2.85", "_attr_state"),
630 | "running_state": ("14.4.85", "_attr_state"),
631 | "position": ("1.1.85", "_attr_current_cover_position"),
632 | },
633 | },
634 | }, {
635 | "sensor": {
636 | MK_INIT_PARAMS: {
637 | MK_HASS_NAME: "battery",
638 | "device_class": DEVICE_CLASS_BATTERY,
639 | "state_class": "measurement",
640 | "unit_of_measurement": PERCENTAGE
641 | },
642 | MK_RESOURCES: {"battery": ("8.0.2001", "_attr_native_value")},
643 | }
644 | }
645 | ]
646 | }, {
647 | 'lumi.sensor_switch': ["Xiaomi", "Button", "WXKG01LM"],
648 | 'lumi.sensor_switch.aq2': ["Aqara", "Button", "WXKG11LM"],
649 | 'lumi.remote.b1acn01': ["Aqara", "Button", "WXKG11LM"],
650 | 'lumi.remote.b1acn02': ["Aqara", "Button", "WXKG12LM"],
651 | 'lumi.sensor_switch.aq3': ["Aqara", "Shake Button", "WXKG12LM"],
652 | 'lumi.sensor_86sw1': ["Aqara", "Single Wall Button", "WXKG03LM"],
653 | 'lumi.remote.b186acn01': ["Aqara", "Single Wall Button", "WXKG03LM"],
654 | 'lumi.remote.b186acn02': ["Aqara", "Single Wall Button D1", "WXKG06LM"],
655 | 'lumi.remote.b18ac1': ["Aqara", "Single Wall Button H1", "WXKG14LM"],
656 | 'lumi.remote.acn003': ["Aqara", "Single Wall Button E1", "WXKG16LM"],
657 | # 无线开关 T1(贴墙式单键)
658 | 'lumi.remote.b186acn03': ["Aqara", "Single Wall Button T1", "WXKG03LM"],
659 | 'params': [
660 | {
661 | "sensor": {
662 | MK_INIT_PARAMS: {
663 | MK_HASS_NAME: "battery",
664 | "device_class": DEVICE_CLASS_BATTERY,
665 | "state_class": "measurement",
666 | "unit_of_measurement": PERCENTAGE
667 | },
668 | MK_RESOURCES: {"battery": ("8.0.2001", "_attr_native_value")},
669 | }
670 | }, {
671 | "sensor": {
672 | MK_INIT_PARAMS: {
673 | MK_HASS_NAME: "action",
674 | "device_class": "",
675 | "state_class": "",
676 | "unit_of_measurement": ""
677 | },
678 | MK_RESOURCES: {"button": ("13.1.85", "_attr_native_value")},
679 | }
680 | }
681 | ]
682 | }, {
683 | 'lumi.sensor_86sw2': ["Aqara", "Double Wall Button", "WXKG02LM"],
684 | 'lumi.remote.b286acn01': ["Aqara", "Double Wall Button", "WXKG02LM"],
685 | 'lumi.sensor_86sw2.es1': ["Aqara", "Double Wall Button", "WXKG02LM"],
686 | 'lumi.remote.b286acn02': ["Aqara", "Double Wall Button D1", "WXKG07LM"],
687 | 'lumi.remote.b286opcn01': ["Aqara", "Opple Two Button", "WXCJKG11LM"],
688 | 'lumi.remote.b486opcn01': ["Aqara", "Opple Four Button", "WXCJKG12LM"],
689 | 'lumi.remote.b686opcn01': ["Aqara", "Opple Six Button", "WXCJKG13LM"],
690 | 'lumi.remote.b28ac1': ["Aqara", "Double Wall Button H1", "WXKG15LM"],
691 | 'lumi.remote.acn004': ["Aqara", "Double Wall Button E1", "WXKG17LM"],
692 | # 无线开关 T1(贴墙式双键)
693 | 'lumi.remote.b286acn03': ["Aqara", "Double Wall Button T1", "WXKG04LM"],
694 | 'params': [
695 | {
696 | "sensor": {
697 | MK_INIT_PARAMS: {
698 | MK_HASS_NAME: "battery",
699 | "device_class": DEVICE_CLASS_BATTERY,
700 | "state_class": "measurement",
701 | "unit_of_measurement": PERCENTAGE
702 | },
703 | MK_RESOURCES: {"battery": ("8.0.2001", "_attr_native_value")},
704 | }
705 | }, {
706 | "sensor": {
707 | MK_INIT_PARAMS: {
708 | MK_HASS_NAME: "action",
709 | "device_class": "",
710 | "state_class": "",
711 | "unit_of_measurement": ""
712 | },
713 | MK_RESOURCES: {"button": ("13.{}.85", "_attr_native_value")},
714 | }
715 | }
716 | ]
717 | }, {
718 | 'lumi.remote.rkba01': ["Aqara", "Smart Knob H1", "ZNXNKG02LM"],
719 | 'params': [
720 | {
721 | "sensor": {
722 | MK_INIT_PARAMS: {
723 | MK_HASS_NAME: "battery",
724 | "device_class": DEVICE_CLASS_BATTERY,
725 | "state_class": "measurement",
726 | "unit_of_measurement": PERCENTAGE
727 | },
728 | MK_RESOURCES: {"battery": ("8.0.2001", "_attr_native_value")},
729 | }
730 | }, {
731 | "sensor": {
732 | MK_INIT_PARAMS: {
733 | MK_HASS_NAME: "action",
734 | "device_class": "",
735 | "state_class": "",
736 | "unit_of_measurement": ""
737 | },
738 | MK_RESOURCES: {
739 | "button": ("13.1.85", "_attr_native_value"),
740 | "rotate_angle": ("0.24.85", "_attr_rotate_angle"),
741 | "action_duration": ("0.25.85", "_attr_action_duration"),
742 | "rotate_angle_w_hold": ("0.29.85", "_attr_rotate_angle_w_hold"),
743 | "action_duration_w_hold": ("0.30.85", "_attr_action_duration_w_hold"),
744 | "fw_ver": ("8.0.2002", "_attr_fw_ver"),
745 | "chip_temperature": ("8.0.2006", "_attr_chip_temperature"),
746 | "lqi": ("8.0.2007", "_attr_lqi"),
747 | },
748 | }
749 | }
750 | ]
751 | }, {
752 | # 高精度人体传感器
753 | 'lumi.motion.agl04': ["Aqara", "Precision Motion Sensor", "RTCGQ13LM"],
754 | 'params': [
755 | {
756 | "binary_sensor": {
757 | MK_INIT_PARAMS: {
758 | MK_HASS_NAME: "motion",
759 | "device_class": DEVICE_CLASS_MOTION
760 | },
761 | MK_RESOURCES: {
762 | "motion": ("3.1.85", "_attr_native_value"),
763 | "chip_temperature": ("8.0.2006", "_attr_chip_temperature"),
764 | "fw_ver": ("8.0.2002", "_attr_fw_ver"),
765 | "lqi": ("8.0.2007", "_attr_lqi"),
766 | "voltage": ("8.0.2008", "_attr_voltage")
767 | },
768 |
769 | }
770 | }, {
771 | "sensor": {
772 | MK_INIT_PARAMS: {
773 | MK_HASS_NAME: "battery",
774 | "device_class": DEVICE_CLASS_BATTERY,
775 | "state_class": "measurement",
776 | "unit_of_measurement": PERCENTAGE
777 | },
778 | MK_RESOURCES: {"battery": ("8.0.2001", "_attr_native_value")},
779 | }
780 | }
781 | ]
782 | }, {
783 | 'lumi.plug': ["Xiaomi", "Plug", "ZNCZ02LM"],
784 | 'lumi.plug.mitw01': ["Xiaomi", "Plug TW", "ZNCZ03LM"],
785 | 'lumi.plug.mmeu01': ["Xiaomi", "Plug EU", "ZNCZ04LM"],
786 | 'lumi.plug.maus01': ["Xiaomi", "Plug US", "ZNCZ12LM"],
787 | 'params': [
788 | {
789 | "switch": {
790 | MK_INIT_PARAMS: {
791 | MK_HASS_NAME: "switch"
792 | },
793 | MK_RESOURCES: {
794 | "toggle": ("4.1.85", "_attr_is_on"),
795 | "power": ("0.12.85", "_attr_current_power_w"),
796 | "energy": ("0.13.85", "_attr_today_energy_kwh"),
797 | "fw_ver": ("8.0.2002", "_attr_fw_ver"),
798 | "chip_temperature": ("8.0.2006", "_attr_chip_temperature"),
799 | "lqi": ("8.0.2007", "_attr_lqi"),
800 | "in_use": ("8.0.2044", "_attr_in_use")
801 | }
802 | }
803 | }, {
804 | "sensor": {
805 | MK_INIT_PARAMS: {
806 | MK_HASS_NAME: "power",
807 | "device_class": DEVICE_CLASS_POWER,
808 | "state_class": "measurement",
809 | "unit_of_measurement": POWER_WATT},
810 | MK_RESOURCES: {"power": ("0.12.85", "_attr_native_value")}
811 | }
812 | }, {
813 | "sensor": {
814 | MK_INIT_PARAMS: {
815 | MK_HASS_NAME: "energy",
816 | "device_class": DEVICE_CLASS_ENERGY,
817 | "state_class": "total_increasing",
818 | "unit_of_measurement": ENERGY_KILO_WATT_HOUR},
819 | MK_RESOURCES: {"energy": ("0.13.85", "_attr_native_value")},
820 | }
821 | }
822 | ]
823 | }, {
824 | 'lumi.sensor_motion.v2': ["Xiaomi", "Motion Sensor", "RTCGQ01LM"],
825 | 'lumi.motion.agl04': ["Aqara", "Precision Motion Sensor", "RTCGQ13LM"],
826 | 'params': [
827 | {
828 | "binary_sensor": {
829 | MK_INIT_PARAMS: {
830 | MK_HASS_NAME: "motion",
831 | "device_class": DEVICE_CLASS_MOTION
832 | },
833 | MK_RESOURCES: {
834 | "motion": ("3.1.85", "_attr_native_value"),
835 | "chip_temperature": ("8.0.2006", "_attr_chip_temperature"),
836 | "fw_ver": ("8.0.2002", "_attr_fw_ver"),
837 | "lqi": ("8.0.2007", "_attr_lqi"),
838 | "voltage": ("8.0.2008", "_attr_voltage")
839 | },
840 |
841 | }
842 | }, {
843 | "sensor": {
844 | MK_INIT_PARAMS: {
845 | MK_HASS_NAME: "battery",
846 | "device_class": DEVICE_CLASS_BATTERY,
847 | "state_class": "measurement",
848 | "unit_of_measurement": PERCENTAGE
849 | },
850 | MK_RESOURCES: {"battery": ("8.0.2001", "_attr_native_value")},
851 | }
852 | }
853 | ]
854 | }, {
855 | 'lumi.motion.agl02': ["Aqara", "Motion Sensor T1", "RTCGQ12LM"],
856 | 'params': [
857 | {
858 | "binary_sensor": {
859 | MK_INIT_PARAMS: {
860 | MK_HASS_NAME: "motion",
861 | "device_class": DEVICE_CLASS_MOTION
862 | },
863 | MK_RESOURCES: {
864 | "motion": ("3.1.85", "_attr_native_value"),
865 | "chip_temperature": ("8.0.2006", "_attr_chip_temperature"),
866 | "fw_ver": ("8.0.2002", "_attr_fw_ver"),
867 | "lqi": ("8.0.2007", "_attr_lqi"),
868 | "voltage": ("8.0.2008", "_attr_voltage")
869 | },
870 |
871 | }
872 | }, {
873 | "sensor": {
874 | MK_INIT_PARAMS: {
875 | MK_HASS_NAME: "battery",
876 | "device_class": DEVICE_CLASS_BATTERY,
877 | "state_class": "measurement",
878 | "unit_of_measurement": PERCENTAGE
879 | },
880 | MK_RESOURCES: {"battery": ("8.0.2001", "_attr_native_value")},
881 | },
882 | }, {
883 | "sensor": {
884 | MK_INIT_PARAMS: {
885 | MK_HASS_NAME: "illuminance",
886 | "device_class": DEVICE_CLASS_ILLUMINANCE,
887 | "state_class": "measurement"
888 | },
889 | MK_RESOURCES: {"illuminance": ("0.3.85", "_attr_native_value")},
890 | }
891 | }
892 | ]
893 | }, {
894 | 'lumi.sensor_magnet': ["Xiaomi", "Door Sensor", "MCCGQ01LM"],
895 | 'lumi.sensor_magnet.aq2': ["Aqara", "Door Sensor", "MCCGQ11LM"],
896 | 'lumi.magnet.agl02': ["Aqara", "Door Sensor T1", "MCCGQ12LM"],
897 | 'lumi.magnet.acn001': ["Aqara", "Door Sensor E1", "MCCGQ14LM"],
898 | 'lumi.magnet.ac01': ["Aqara", "Door Sensor P1", "MCCGQ13LM"],
899 | 'params': [
900 | {
901 | "binary_sensor": {
902 | MK_INIT_PARAMS: {
903 | MK_HASS_NAME: "contact",
904 | "device_class": DEVICE_CLASS_DOOR
905 | },
906 | MK_RESOURCES: {
907 | "status": ("3.1.85", "_attr_native_value"),
908 | "chip_temperature": ("8.0.2006", "_attr_chip_temperature"),
909 | "fw_ver": ("8.0.2002", "_attr_fw_ver"),
910 | "lqi": ("8.0.2007", "_attr_lqi"),
911 | "voltage": ("8.0.2008", "_attr_voltage")
912 | },
913 | }
914 | }, {
915 | "sensor": {
916 | MK_INIT_PARAMS: {
917 | MK_HASS_NAME: "battery",
918 | "device_class": DEVICE_CLASS_BATTERY,
919 | "state_class": "measurement",
920 | "unit_of_measurement": PERCENTAGE
921 | },
922 | MK_RESOURCES: {"battery": ("8.0.2001", "_attr_native_value")},
923 | }
924 | }
925 | ]
926 | }, {
927 | # motion sensor with illuminance
928 | 'lumi.sensor_motion.aq2': ["Aqara", "Motion Sensor", "RTCGQ11LM"],
929 | 'params': [
930 | {
931 | "binary_sensor": {
932 | MK_INIT_PARAMS: {
933 | MK_HASS_NAME: "motion",
934 | "device_class": DEVICE_CLASS_MOTION
935 | },
936 | MK_RESOURCES: {
937 | "motion": ("3.1.85", "_attr_native_value"),
938 | "chip_temperature": ("8.0.2006", "_attr_chip_temperature"),
939 | "fw_ver": ("8.0.2002", "_attr_fw_ver"),
940 | "lqi": ("8.0.2007", "_attr_lqi"),
941 | "voltage": ("8.0.2008", "_attr_voltage")
942 | },
943 |
944 | }
945 | }, {
946 | "sensor": {
947 | MK_INIT_PARAMS: {
948 | MK_HASS_NAME: "battery",
949 | "device_class": DEVICE_CLASS_BATTERY,
950 | "state_class": "measurement",
951 | "unit_of_measurement": PERCENTAGE
952 | },
953 | MK_RESOURCES: {"battery": ("8.0.2001", "_attr_native_value")},
954 | }
955 | }
956 | ]
957 | }, {
958 | # temperature and humidity sensor
959 | 'lumi.sensor_ht': ["Xiaomi", "TH Sensor", "WSDCGQ01LM"],
960 | 'params': [
961 | {
962 | "sensor": {
963 | MK_INIT_PARAMS: {
964 | MK_HASS_NAME: "battery",
965 | "device_class": DEVICE_CLASS_BATTERY,
966 | "state_class": "measurement",
967 | "unit_of_measurement": PERCENTAGE
968 | },
969 | MK_RESOURCES: {"battery": ("8.0.2001", "_attr_native_value")},
970 | }
971 | }, {
972 | "sensor": {
973 | MK_INIT_PARAMS: {
974 | MK_HASS_NAME: "temperature",
975 | "device_class": DEVICE_CLASS_TEMPERATURE,
976 | "state_class": "measurement",
977 | "unit_of_measurement": TEMP_CELSIUS
978 | },
979 | MK_RESOURCES: {"temperature": ("'0.1.85", "_attr_native_value")},
980 | }
981 | }, {
982 | "sensor": {
983 | MK_INIT_PARAMS: {
984 | MK_HASS_NAME: "humidity",
985 | "device_class": DEVICE_CLASS_HUMIDITY,
986 | "state_class": "measurement",
987 | "unit_of_measurement": PERCENTAGE
988 | },
989 | MK_RESOURCES: {"humidity": ("0.2.85", "_attr_native_value")},
990 | }
991 | }
992 | ]
993 | }, {
994 | # temperature, humidity and pressure sensor
995 | 'lumi.weather': ["Aqara", "TH Sensor", "WSDCGQ11LM"],
996 | 'lumi.sensor_ht.agl02': ["Aqara", "TH Sensor", "WSDCGQ12LM"],
997 | 'params': [
998 | {
999 | "sensor": {
1000 | MK_INIT_PARAMS: {
1001 | MK_HASS_NAME: "battery",
1002 | "device_class": DEVICE_CLASS_BATTERY,
1003 | "state_class": "measurement",
1004 | "unit_of_measurement": PERCENTAGE
1005 | },
1006 | MK_RESOURCES: {"battery": ("8.0.2001", "_attr_native_value")},
1007 | }
1008 | }, {
1009 | "sensor": {
1010 | MK_INIT_PARAMS: {
1011 | MK_HASS_NAME: "temperature",
1012 | "device_class": DEVICE_CLASS_TEMPERATURE,
1013 | "state_class": "measurement",
1014 | "unit_of_measurement": TEMP_CELSIUS
1015 | },
1016 | MK_RESOURCES: {"temperature": ("'0.1.85", "_attr_native_value")},
1017 | }
1018 | }, {
1019 | "sensor": {
1020 | MK_INIT_PARAMS: {
1021 | MK_HASS_NAME: "humidity",
1022 | "device_class": DEVICE_CLASS_HUMIDITY,
1023 | "state_class": "measurement",
1024 | "unit_of_measurement": PERCENTAGE
1025 | },
1026 | MK_RESOURCES: {"humidity": ("0.2.85", "_attr_native_value")},
1027 | }
1028 | }, {
1029 | "sensor": {
1030 | MK_INIT_PARAMS: {
1031 | MK_HASS_NAME: "pressure",
1032 | "device_class": DEVICE_CLASS_PRESSURE,
1033 | "state_class": "measurement",
1034 | "unit_of_measurement": PRESSURE_HPA
1035 | },
1036 | MK_RESOURCES: {"pressure": ("0.3.85", "_attr_native_value")},
1037 | }
1038 | }
1039 | ]
1040 | }, {
1041 | # water leak sensor
1042 | 'lumi.sensor_wleak.aq1': ["Aqara", "Water Leak Sensor", "SJCGQ11LM"],
1043 | 'lumi.flood.agl02': ["Aqara", "Water Leak Sensor T1", "SJCGQ12LM"],
1044 | 'lumi.flood.acn001': ["Aqara", "Water Leak Sensor E1", "SJCGQ13LM"],
1045 | 'params': [
1046 | {
1047 | "binary_sensor": {
1048 | MK_INIT_PARAMS: {
1049 | MK_HASS_NAME: "moisture",
1050 | "device_class": DEVICE_CLASS_MOISTURE
1051 | },
1052 | MK_RESOURCES: {
1053 | "moisture": ("3.1.85", "_attr_native_value"),
1054 | "chip_temperature": ("8.0.2006", "_attr_chip_temperature"),
1055 | "fw_ver": ("8.0.2002", "_attr_fw_ver"),
1056 | "lqi": ("8.0.2007", "_attr_lqi"),
1057 | "voltage": ("8.0.2008", "_attr_voltage")
1058 | },
1059 | }
1060 | }, {
1061 | "sensor": {
1062 | MK_INIT_PARAMS: {
1063 | MK_HASS_NAME: "battery",
1064 | "device_class": DEVICE_CLASS_BATTERY,
1065 | "state_class": "measurement",
1066 | "unit_of_measurement": PERCENTAGE
1067 | },
1068 | MK_RESOURCES: {"battery": ("8.0.2001", "_attr_native_value")},
1069 | }
1070 | }
1071 | ]
1072 | }, {
1073 | 'lumi.sen_ill.agl01': ["Aqara", "Light Sensor T1", "GZCGQ11LM"],
1074 | 'lumi.sen_ill.mgl01': ["Xiaomi", "Light Sensor", "GZCGQ01LM"],
1075 | 'params': [
1076 | {
1077 | "sensor": {
1078 | MK_INIT_PARAMS: {
1079 | MK_HASS_NAME: "illuminance",
1080 | "device_class": DEVICE_CLASS_ILLUMINANCE,
1081 | "state_class": "measurement",
1082 | "unit_of_measurement": LIGHT_LUX
1083 | },
1084 | MK_RESOURCES: {"illumination": ("0.3.85", "_attr_native_value")},
1085 | }
1086 | }, {
1087 | "sensor": {
1088 | MK_INIT_PARAMS: {
1089 | MK_HASS_NAME: "battery",
1090 | "device_class": DEVICE_CLASS_BATTERY,
1091 | "state_class": "measurement",
1092 | "unit_of_measurement": PERCENTAGE
1093 | },
1094 | MK_RESOURCES: {"battery": ("8.0.2001", "_attr_native_value")},
1095 | }
1096 | }
1097 | ]
1098 | }, {
1099 | 'lumi.sensor_smoke': ["Honeywell", "Smoke Sensor", "JTYJ-GD-01LM/BW"],
1100 | 'params': [
1101 | {
1102 | "binary_sensor": {
1103 | MK_INIT_PARAMS: {
1104 | MK_HASS_NAME: "smoke"
1105 | },
1106 | MK_RESOURCES: {
1107 | "smoke": ("13.1.85", "_attr_native_value"),
1108 | "chip_temperature": ("8.0.2006", "_attr_chip_temperature"),
1109 | "fw_ver": ("8.0.2002", "_attr_fw_ver"),
1110 | "lqi": ("8.0.2007", "_attr_lqi"),
1111 | "voltage": ("8.0.2008", "_attr_voltage")
1112 | },
1113 | }
1114 | }, {
1115 | "sensor": {
1116 | MK_INIT_PARAMS: {
1117 | MK_HASS_NAME: "smoke_density",
1118 | "device_class": "smoke",
1119 | "state_class": "measurement",
1120 | "unit_of_measurement": CONCENTRATION_PARTS_PER_MILLION
1121 | },
1122 | MK_RESOURCES: {"smoke": ("0.1.85", "_attr_native_value")},
1123 | }
1124 | }, {
1125 | "sensor": {
1126 | MK_INIT_PARAMS: {
1127 | MK_HASS_NAME: "battery",
1128 | "device_class": DEVICE_CLASS_BATTERY,
1129 | "state_class": "measurement",
1130 | "unit_of_measurement": PERCENTAGE
1131 | },
1132 | MK_RESOURCES: {"battery": ("8.0.2001", "_attr_native_value")},
1133 | }
1134 | }
1135 | ]
1136 | }, {
1137 | 'lumi.sensor_natgas': ["Honeywell", "Gas Sensor", "JTQJ-BF-01LM/BW"],
1138 | 'params': [
1139 | {
1140 | "binary_sensor": {
1141 | MK_INIT_PARAMS: {
1142 | MK_HASS_NAME: "natgas"
1143 | },
1144 | MK_RESOURCES: {
1145 | "gas": ("13.1.85", "_attr_native_value"),
1146 | "chip_temperature": ("8.0.2006", "_attr_chip_temperature"),
1147 | "fw_ver": ("8.0.2002", "_attr_fw_ver"),
1148 | "lqi": ("8.0.2007", "_attr_lqi"),
1149 | "voltage": ("8.0.2008", "_attr_voltage")
1150 | },
1151 | }
1152 | }, {
1153 | "sensor": {
1154 | MK_INIT_PARAMS: {
1155 | MK_HASS_NAME: "natgas",
1156 | "device_class": DEVICE_CLASS_GAS,
1157 | "state_class": "measurement",
1158 | "unit_of_measurement": CONCENTRATION_PARTS_PER_MILLION
1159 | },
1160 | MK_RESOURCES: {"gas": ("0.1.85", "_attr_native_value")},
1161 | }
1162 | }
1163 | ]
1164 | }, {
1165 | 'lumi.airmonitor.acn01': ["Aqara", "Smart TVOC Air Quality Monitor", "VOCKQJK11LM"],
1166 | 'params': [
1167 | {
1168 | "sensor": {
1169 | MK_INIT_PARAMS: {
1170 | MK_HASS_NAME: "battery",
1171 | "device_class": DEVICE_CLASS_BATTERY,
1172 | "state_class": "measurement",
1173 | "unit_of_measurement": PERCENTAGE
1174 | },
1175 | MK_RESOURCES: {"battery": ("8.0.2001", "_attr_native_value")},
1176 | }
1177 | }, {
1178 | "sensor": {
1179 | MK_INIT_PARAMS: {
1180 | MK_HASS_NAME: "temperature",
1181 | "device_class": DEVICE_CLASS_TEMPERATURE,
1182 | "state_class": "measurement",
1183 | "unit_of_measurement": TEMP_CELSIUS
1184 | },
1185 | MK_RESOURCES: {"temperature": ("'0.1.85", "_attr_native_value")},
1186 | }
1187 | }, {
1188 | "sensor": {
1189 | MK_INIT_PARAMS: {
1190 | MK_HASS_NAME: "humidity",
1191 | "device_class": DEVICE_CLASS_HUMIDITY,
1192 | "state_class": "measurement",
1193 | "unit_of_measurement": PERCENTAGE
1194 | },
1195 | MK_RESOURCES: {"humidity": ("0.2.85", "_attr_native_value")},
1196 | }
1197 | }, {
1198 | "sensor": {
1199 | MK_INIT_PARAMS: {
1200 | MK_HASS_NAME: "tvoc",
1201 | "device_class": DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
1202 | "state_class": "measurement",
1203 | "unit_of_measurement": ''
1204 | },
1205 | MK_RESOURCES: {"tvoc": ("0.3.85", "_attr_native_value")},
1206 | }
1207 | }, {
1208 | "air_quality": {
1209 | MK_INIT_PARAMS: {
1210 | MK_HASS_NAME: "tvoc_level",
1211 | "device_class": '',
1212 | "state_class": "measurement",
1213 | "unit_of_measurement": ''
1214 | },
1215 | MK_RESOURCES: {"tvoc_level": ("13.1.85", "_attr_tvoc_level")},
1216 | }
1217 | }
1218 | ]
1219 | }, {
1220 | 'aqara.lock.bzacn3': ["Aqara", "Door Lock N100", "ZNMS16LM"],
1221 | 'aqara.lock.bzacn4': ["Aqara", "Door Lock N100", "ZNMS16LM"],
1222 | 'params': [
1223 | {
1224 | "sensor": {
1225 | MK_INIT_PARAMS: {
1226 | MK_HASS_NAME: "battery",
1227 | "device_class": DEVICE_CLASS_BATTERY,
1228 | "state_class": "measurement",
1229 | "unit_of_measurement": PERCENTAGE
1230 | },
1231 | MK_RESOURCES: {"battery": ("13.56.85", "_attr_native_value")},
1232 | }
1233 | }
1234 | ]
1235 | }, {
1236 | 'aqara.lock.wbzac1': ["Aqara", "Door Lock P100", "ZNMS19LM"],
1237 | 'params': [
1238 | {
1239 | "sensor": {
1240 | MK_INIT_PARAMS: {
1241 | MK_HASS_NAME: "li_battery",
1242 | "device_class": DEVICE_CLASS_BATTERY,
1243 | "state_class": "measurement",
1244 | "unit_of_measurement": PERCENTAGE
1245 | },
1246 | MK_RESOURCES: {"li_battery": ("13.32.85", "_attr_native_value")},
1247 | }
1248 | }, {
1249 | "sensor": {
1250 | MK_INIT_PARAMS: {
1251 | MK_HASS_NAME: "battery",
1252 | "device_class": DEVICE_CLASS_BATTERY,
1253 | "state_class": "measurement",
1254 | "unit_of_measurement": PERCENTAGE
1255 | },
1256 | MK_RESOURCES: {"battery": ("13.37.85", "_attr_native_value")},
1257 | }
1258 | }, {
1259 | "sensor": {
1260 | MK_INIT_PARAMS: {
1261 | MK_HASS_NAME: "lock",
1262 | "device_class": "",
1263 | "state_class": "",
1264 | "unit_of_measurement": ""
1265 | },
1266 | MK_RESOURCES: {"lock_state": ("13.17.85", "_attr_native_value")},
1267 | }
1268 | }
1269 | ]
1270 | }]
1271 |
1272 | SPECIAL_DEVICES_INFO = {
1273 | # VRF空调控制器
1274 | "lumi.airrtc.vrfegl01": {
1275 | "toggle": {0: "on", 1: "off"},
1276 | "hvac_mode": {
1277 | 0: "heat",
1278 | 1: "cool",
1279 | 2: "auto",
1280 | 3: "dry",
1281 | 4: "fan_only",
1282 | },
1283 | "fan_mode": {0: "low", 1: "middle", 2: "high", 3: "auto"},
1284 | "swing_mode": {0: "horizontal", 1: "vertical", 2: "both"},
1285 | "swing_toggle": {1: "off"},
1286 | }
1287 | }
1288 |
--------------------------------------------------------------------------------
/custom_components/aqara_bridge/core/const.py:
--------------------------------------------------------------------------------
1 | """Constants for the Aqara Bridge component."""
2 | DOMAIN = "aqara_bridge"
3 |
4 | # Config flow fields
5 | CONF_FIELD_ACCOUNT = "field_account"
6 | CONF_FIELD_COUNTRY_CODE = "field_country_code"
7 | CONF_FIELD_AUTH_CODE = "field_auth_code"
8 | CONF_FIELD_SELECTED_DEVICES = "field_selected_devices"
9 | CONF_FIELD_REFRESH_TOKEN = "field_refresh_token"
10 | CONF_OCCUPANCY_TIMEOUT = 'occupancy_timeout'
11 |
12 | # Cloud
13 | SERVER_COUNTRY_CODES = ["CN", "USA", "KR", "RU", "GER"]
14 | SERVER_COUNTRY_CODES_DEFAULT = "CN"
15 |
16 | # CONFIG ENTRY
17 | CONF_ENTRY_AUTH_ACCOUNT = "account"
18 | CONF_ENTRY_AUTH_ACCOUNT_TYPE = "account_type"
19 | CONF_ENTRY_AUTH_COUNTRY_CODE = "country_code"
20 | CONF_ENTRY_AUTH_EXPIRES_IN = "expires_in"
21 | CONF_ENTRY_AUTH_EXPIRES_TIME = "expires_datetime"
22 | CONF_ENTRY_AUTH_ACCESS_TOKEN = "access_token"
23 | CONF_ENTRY_AUTH_REFRESH_TOKEN = "refresh_token"
24 | CONF_ENTRY_AUTH_OPENID = "open_id"
25 |
26 | # HASS DATA
27 | HASS_DATA_AUTH_ENTRY_ID = "auth_entry_id"
28 | HASS_DATA_AIOTCLOUD = "aiotcloud"
29 | HASS_DATA_AIOT_MANAGER = "aiot_manager"
30 |
31 | CONF_DEBUG = "debug"
32 | CONF_STATS = "stats"
33 |
34 | OPT_DEBUG = {
35 | 'true': "Basic logs",
36 | 'verbose': "Verbose logs",
37 | 'msg': "msg logs"
38 | }
39 |
40 | ATTR_CHIP_TEMPERATURE = "chip_temperature"
41 | ATTR_FW_VER = "fw_ver"
42 | ATTR_LQI = "lqi"
43 | ATTR_VOLTAGE = "voltage"
44 |
45 | PROP_TO_ATTR_BASE = {
46 | "chip_temperature": ATTR_CHIP_TEMPERATURE,
47 | "fw_ver": ATTR_FW_VER,
48 | "lqi": ATTR_LQI,
49 | "voltage": ATTR_VOLTAGE
50 | }
51 |
52 | # Air Quality Monitor
53 | ATTR_CO2E = "carbon_dioxide_equivalent"
54 | ATTR_TVOC = "total_volatile_organic_compounds"
55 | ATTR_HUMIDITY = "humidity"
56 |
57 | # Switch Sensor
58 | # https://github.com/Koenkk/zigbee-herdsman-converters/blob/master/converters/fromZigbee.js#L4738
59 | BUTTON = {
60 | '1': 'single',
61 | '2': 'double',
62 | '3': 'triple',
63 | '4': 'quadruple',
64 | '16': 'hold',
65 | '17': 'release',
66 | '18': 'shake',
67 | '20': 'reversing_rotate',
68 | '21': 'hold_rotate',
69 | '22': 'clockwise',
70 | '23': 'counterclockwise',
71 | '24': 'hold_clockwise',
72 | '25': 'hold_counterclockwise',
73 | '26': 'rotate',
74 | '27': 'hold_rotate',
75 | '128': 'many'
76 | }
77 |
78 | BUTTON_BOTH = {
79 | '4': 'single',
80 | '5': 'double',
81 | '6': 'triple',
82 | '16': 'hold',
83 | '17': 'release',
84 | }
85 |
86 | VIBRATION = {
87 | '1': 'vibration',
88 | '2': 'tilt',
89 | '3': 'drop',
90 | }
91 |
92 | CUBE = {
93 | '0': 'flip90',
94 | '1': 'flip180',
95 | '2': 'move',
96 | '3': 'knock',
97 | '4': 'quadruple',
98 | '16': 'rotate',
99 | '20': 'shock',
100 | '28': 'hold',
101 | 'move': 'move',
102 | 'flip90': 'flip90',
103 | 'flip180': 'flip180',
104 | 'rotate': 'rotate',
105 | 'alert': 'alert',
106 | 'shake_air': 'shock',
107 | 'tap_twice': 'knock'
108 | }
109 |
--------------------------------------------------------------------------------
/custom_components/aqara_bridge/core/utils.py:
--------------------------------------------------------------------------------
1 | """ the Aqara Bridge utils."""
2 | import logging
3 | import re
4 | import uuid
5 | from datetime import datetime
6 | from aiohttp import web
7 |
8 | from homeassistant.components.http import HomeAssistantView
9 | from homeassistant.helpers.typing import HomeAssistantType
10 |
11 |
12 | TITLE = "Aqara Bridge Debug"
13 | NOTIFY_TEXT = 'Open Log'
14 | HTML = (f'{TITLE}'
15 | ''
16 | '%s
')
17 |
18 | # These code are reference to @AlexxIT. It is very useful to help debug.
19 | class AqaraBridgeDebug(logging.Handler, HomeAssistantView):
20 | # pylint: disable=abstract-method, arguments-differ
21 | """ debug handler """
22 | name = "bridge_debug"
23 | requires_auth = False
24 |
25 | text = ''
26 |
27 | def __init__(self, hass: HomeAssistantType):
28 | super().__init__()
29 |
30 | # random url because without authorization!!!
31 | self.url = "/{}".format(uuid.uuid4())
32 |
33 | hass.http.register_view(self)
34 | hass.components.persistent_notification.async_create(
35 | NOTIFY_TEXT % self.url, title=TITLE)
36 |
37 | def handle(self, rec: logging.LogRecord) -> None:
38 | date_time = datetime.fromtimestamp(rec.created).strftime(
39 | "%Y-%m-%d %H:%M:%S")
40 | module = 'main' if rec.module == '__init__' else rec.module
41 | self.text = "{} {} {} {} {}\n".format(
42 | self.text, date_time, rec.levelname, module, rec.msg)
43 |
44 | async def get(self, request: web.Request):
45 | """ for shortcut """
46 | try:
47 | if 'c' in request.query:
48 | self.text = ''
49 |
50 | if 'q' in request.query or 't' in request.query:
51 | lines = self.text.split('\n')
52 |
53 | if 'q' in request.query:
54 | reg = re.compile(fr"({request.query['q']})", re.IGNORECASE)
55 | lines = [p for p in lines if reg.search(p)]
56 |
57 | if 't' in request.query:
58 | tail = int(request.query['t'])
59 | lines = lines[-tail:]
60 |
61 | body = '\n'.join(lines)
62 | else:
63 | body = self.text
64 |
65 | reload = request.query.get('r', '')
66 | return web.Response(text=HTML % (reload, body),
67 | content_type="text/html")
68 |
69 | except Exception:
70 | return web.Response(status=500)
71 |
--------------------------------------------------------------------------------
/custom_components/aqara_bridge/cover.py:
--------------------------------------------------------------------------------
1 | import logging
2 | from homeassistant.components.cover import CoverEntity
3 |
4 | from .core.aiot_manager import (
5 | AiotManager,
6 | AiotEntityBase,
7 | )
8 | from .core.const import DOMAIN, HASS_DATA_AIOT_MANAGER
9 |
10 | TYPE = "cover"
11 |
12 | _LOGGER = logging.getLogger(__name__)
13 |
14 | DATA_KEY = f"{TYPE}.{DOMAIN}"
15 |
16 |
17 | async def async_setup_entry(hass, config_entry, async_add_entities):
18 | manager: AiotManager = hass.data[DOMAIN][HASS_DATA_AIOT_MANAGER]
19 | cls_entities = {
20 | "default": AiotCoverEntity
21 | }
22 | await manager.async_add_entities(
23 | config_entry, TYPE, cls_entities, async_add_entities
24 | )
25 |
26 |
27 | class AiotCoverEntity(AiotEntityBase, CoverEntity):
28 | def __init__(self, hass, device, res_params, channel=None, **kwargs):
29 | AiotEntityBase.__init__(self, hass, device, res_params, TYPE, channel, **kwargs)
30 | self._attr_is_closed = None
31 |
32 | async def async_open_cover(self, **kwargs):
33 | return await super().async_open_cover(**kwargs)
34 |
35 | async def async_close_cover(self, **kwargs):
36 | return await super().async_close_cover(**kwargs)
37 |
38 | async def async_set_cover_position(self, **kwargs):
39 | return await super().async_set_cover_position(**kwargs)
40 |
41 | async def async_stop_cover(self, **kwargs):
42 | return await super().async_stop_cover(**kwargs)
43 |
44 | async def async_open_cover_tilt(self, **kwargs):
45 | return await super().async_open_cover_tilt(**kwargs)
46 |
47 | async def async_close_cover_tilt(self, **kwargs):
48 | return await super().async_close_cover_tilt(**kwargs)
49 |
50 | async def async_set_cover_tilt_position(self, **kwargs):
51 | return await super().async_set_cover_tilt_position(**kwargs)
52 |
53 | async def async_stop_cover_tilt(self, **kwargs):
54 | return await super().async_stop_cover_tilt(**kwargs)
55 |
56 | def convert_attr_to_res(self, res_name, attr_value):
57 | if res_name == "curtain_state":
58 | print(attr_value)
59 | elif res_name == "position":
60 | return str(attr_value)
61 | return super().convert_attr_to_res(res_name, attr_value)
62 |
63 | def convert_res_to_attr(self, res_name, res_value):
64 | if res_name == "curtain_state":
65 | print(res_value)
66 | elif res_name == "position":
67 | return int(res_value)
68 | return super().convert_res_to_attr(res_name, res_value)
69 |
70 | def __setattr__(self, name: str, value):
71 | if name == "_attr_state":
72 | print(value)
73 | return super().__setattr__(name, value)
74 |
--------------------------------------------------------------------------------
/custom_components/aqara_bridge/light.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import homeassistant.util.color as color_util
3 | from homeassistant.components.light import LightEntity
4 |
5 | from .core.aiot_manager import (
6 | AiotManager,
7 | AiotToggleableEntityBase,
8 | )
9 | from .core.const import DOMAIN, HASS_DATA_AIOT_MANAGER
10 |
11 | TYPE = "light"
12 |
13 | _LOGGER = logging.getLogger(__name__)
14 |
15 | DATA_KEY = f"{TYPE}.{DOMAIN}"
16 |
17 |
18 | async def async_setup_entry(hass, config_entry, async_add_entities):
19 | manager: AiotManager = hass.data[DOMAIN][HASS_DATA_AIOT_MANAGER]
20 | cls_entities = {
21 | "default": AiotLightEntity
22 | }
23 | await manager.async_add_entities(
24 | config_entry, TYPE, cls_entities, async_add_entities
25 | )
26 |
27 |
28 | class AiotLightEntity(AiotToggleableEntityBase, LightEntity):
29 | def __init__(self, hass, device, res_params, channel=None, **kwargs):
30 | AiotToggleableEntityBase.__init__(
31 | self, hass, device, res_params, TYPE, channel, **kwargs
32 | )
33 | self._attr_color_mode = kwargs.get("color_mode")
34 |
35 | async def async_turn_on(self, **kwargs):
36 | """Turn the specified light on."""
37 | hs_color = kwargs.get("hs_color")
38 | if hs_color:
39 | await self.async_set_resource("color", hs_color)
40 |
41 | brightness = kwargs.get("brightness")
42 | if brightness:
43 | await self.async_set_resource("brightness", brightness)
44 |
45 | color_temp = kwargs.get("color_temp")
46 | if color_temp:
47 | await self.async_set_resource("color_temp", color_temp)
48 |
49 | await super().async_turn_on(**kwargs)
50 |
51 | def convert_attr_to_res(self, res_name, attr_value):
52 | if res_name == "brightness":
53 | # attr_value:0-255,亮度
54 | return int(attr_value * 100 / 255)
55 | elif res_name == "color":
56 | # attr_value:hs颜色
57 | rgb_color = color_util.color_hs_to_RGB(*attr_value)
58 | return int(
59 | "{}{}{}{}".format(
60 | hex(int(self.brightness * 100 / 255))[2:4].zfill(2),
61 | hex(rgb_color[0])[2:4].zfill(2),
62 | hex(rgb_color[1])[2:4].zfill(2),
63 | hex(rgb_color[2])[2:4].zfill(2),
64 | ),
65 | 16,
66 | )
67 | elif res_name == "color_temp":
68 | # attr_value:color temp
69 | return int(attr_value)
70 | return super().convert_attr_to_res(res_name, attr_value)
71 |
72 | def convert_res_to_attr(self, res_name, res_value):
73 | if res_name == "brightness":
74 | # res_value:0-100,亮度百分比
75 | return int(int(res_value) * 255 / 100)
76 | elif res_name == "color":
77 | # res_value:十进制整数字符串
78 | argb = hex(int(res_value))
79 | return color_util.color_RGB_to_hs(
80 | int(argb[4:6], 16), int(argb[6:8], 16), int(argb[8:10], 16)
81 | )
82 | elif res_name == "color_temp":
83 | # res_value:153-500
84 | return int(res_value)
85 | return super().convert_res_to_attr(res_name, res_value)
86 |
--------------------------------------------------------------------------------
/custom_components/aqara_bridge/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "domain": "aqara_bridge",
3 | "name": "Aqara Bridge",
4 | "config_flow": true,
5 | "iot_class": "cloud_polling",
6 | "documentation": "",
7 | "issue_tracker": "",
8 | "requirements": ["rocketmq"],
9 | "dependencies": [],
10 | "codeowners": ["@angeljanne"],
11 | "version": "v0.1.0"
12 | }
--------------------------------------------------------------------------------
/custom_components/aqara_bridge/remote.py:
--------------------------------------------------------------------------------
1 | """ Aqara Bridge remote """
2 | import asyncio
3 | import time
4 | import voluptuous as vol
5 |
6 | from homeassistant.helpers import config_validation as cv
7 | from homeassistant.components.remote import (
8 | ATTR_DELAY_SECS,
9 | ATTR_NUM_REPEATS,
10 | DEFAULT_DELAY_SECS,
11 | RemoteEntity
12 | )
13 | from homeassistant.const import CONF_TIMEOUT
14 |
15 | from .core.aiot_manager import AiotManager, AiotEntityBase
16 | from .core.const import DOMAIN, HASS_DATA_AIOT_MANAGER
17 |
18 | TYPE = "remote"
19 |
20 | DATA_KEY = f"{TYPE}.{DOMAIN}"
21 |
22 |
23 | async def async_setup_entry(hass, config_entry, async_add_entities):
24 | manager: AiotManager = hass.data[DOMAIN][HASS_DATA_AIOT_MANAGER]
25 | cls_entities = {
26 | "pair": AiotRemotePair,
27 | "ir": AiotRemoteIrda,
28 | "default": AiotRemoteEntity
29 | }
30 | await manager.async_add_entities(
31 | config_entry, TYPE, cls_entities, async_add_entities
32 | )
33 |
34 |
35 | class AiotRemoteEntity(AiotEntityBase, RemoteEntity):
36 | def __init__(self, hass, device, res_params, **kwargs):
37 | AiotEntityBase.__init__(
38 | self, hass, device, res_params, TYPE, **kwargs
39 | )
40 | self._attr_is_on = False
41 |
42 | async def async_turn_on(self, **kwargs):
43 | await self.async_set_resource("remote", True)
44 |
45 | async def async_turn_off(self, **kwargs):
46 | await self.async_set_resource("remote", False)
47 |
48 | def convert_attr_to_res(self, res_name, attr_value):
49 | return super().convert_attr_to_res(res_name, attr_value)
50 |
51 | def convert_res_to_attr(self, res_name, res_value):
52 | return super().convert_res_to_attr(res_name, res_value)
53 |
54 |
55 | class AiotRemotePair(AiotEntityBase, RemoteEntity):
56 | def __init__(self, hass, device, res_params, **kwargs):
57 | AiotEntityBase.__init__(
58 | self, hass, device, res_params, TYPE, **kwargs
59 | )
60 | self._attr_is_on = False
61 |
62 | async def async_turn_on(self, **kwargs):
63 | await self.async_device_connection(True)
64 |
65 | async def async_turn_off(self, **kwargs):
66 | await self.async_device_connection(False)
67 |
68 |
69 | class AiotRemoteIrda(AiotEntityBase, RemoteEntity):
70 | def __init__(self, hass, device, res_params, **kwargs):
71 | AiotEntityBase.__init__(
72 | self, hass, device, res_params, TYPE, **kwargs
73 | )
74 | self._attr_is_on = False
75 |
76 | async def async_turn_on(self, **kwargs):
77 | """Turn the remote on."""
78 |
79 | async def async_turn_off(self, **kwargs):
80 | """Turn the remote off."""
81 |
82 | async def async_send_command(self, command, **kwargs):
83 | """ send command """
84 | num_repeats = kwargs.get(ATTR_NUM_REPEATS, 1)
85 | delay = kwargs.get(ATTR_DELAY_SECS, DEFAULT_DELAY_SECS)
86 |
87 | for _ in range(num_repeats):
88 | await self.async_set_resource("irda", command)
89 | time.sleep(delay)
90 |
91 | async def async_learn_command(self, **kwargs):
92 | """Handle a learn command."""
93 | timeout = kwargs.get(CONF_TIMEOUT, 10)
94 |
95 | resp = await self.async_infrared_learn(True, 20)
96 | if isinstance(resp, dict):
97 | keyid = resp['keyId']
98 |
99 | start_time = utcnow()
100 | while (utcnow() - start_time) < timedelta(seconds=timeout):
101 | message = await self.hass.async_add_executor_job(
102 | self.async_received_learnresult, keyid)
103 | self.debug("Message received from device: '%s'", message)
104 |
105 | if isinstance(message, dict):
106 | log_msg = "Received command is: {}".format(message['ircode'])
107 | self.hass.components.persistent_notification.async_create(
108 | log_msg, title="Aqara Remote"
109 | )
110 | return
111 |
112 | if message is None:
113 | await self.async_infrared_learn(False)
114 |
115 | await asyncio.sleep(1)
116 |
--------------------------------------------------------------------------------
/custom_components/aqara_bridge/sensor.py:
--------------------------------------------------------------------------------
1 | import time
2 | from homeassistant.components.sensor import SensorEntity
3 |
4 | from .core.aiot_manager import (
5 | AiotManager,
6 | AiotEntityBase,
7 | )
8 | from .core.const import (
9 | BUTTON,
10 | BUTTON_BOTH,
11 | CUBE,
12 | DOMAIN,
13 | HASS_DATA_AIOT_MANAGER,
14 | PROP_TO_ATTR_BASE,
15 | VIBRATION
16 | )
17 |
18 | TYPE = "sensor"
19 |
20 | DATA_KEY = f"{TYPE}.{DOMAIN}"
21 |
22 | ATTR_ROTATE_ANGLE = "rotate_angle"
23 | ATTR_ACTION_DURATION = "action_duration"
24 | ATTR_ROTATE_ANGLE_W_HOLD = "rotate_angle_w_hold"
25 | ATTR_ACTION_DURATION_W_HOLD = "action_duration_w_hold"
26 |
27 | PROP_TO_ATTR = {
28 | "rotate_angle": ATTR_ROTATE_ANGLE,
29 | "action_duration": ATTR_ACTION_DURATION,
30 | "rotate_angle_w_hold": ATTR_ROTATE_ANGLE_W_HOLD,
31 | "action_duration_w_hold": ATTR_ACTION_DURATION_W_HOLD
32 | }
33 |
34 |
35 | async def async_setup_entry(hass, config_entry, async_add_entities):
36 | manager: AiotManager = hass.data[DOMAIN][HASS_DATA_AIOT_MANAGER]
37 | cls_entities = {
38 | "action": AiotActionSensor,
39 | "default": AiotSensorEntity
40 | }
41 | await manager.async_add_entities(
42 | config_entry, TYPE, cls_entities, async_add_entities
43 | )
44 |
45 |
46 | class AiotSensorEntity(AiotEntityBase, SensorEntity):
47 | def __init__(self, hass, device, res_params, channel=None, **kwargs):
48 | AiotEntityBase.__init__(self, hass, device, res_params, TYPE, channel, **kwargs)
49 | self._attr_state_class = kwargs.get("state_class")
50 | self._attr_name = f"{self._attr_name} {self._attr_device_class}"
51 |
52 | def convert_res_to_attr(self, res_name, res_value):
53 | if res_name == "battry":
54 | return int(res_value)
55 | if res_name == "energy":
56 | return round(float(res_value) / 1000.0, 3)
57 | return super().convert_res_to_attr(res_name, res_value)
58 |
59 |
60 | class AiotActionSensor(AiotSensorEntity, SensorEntity):
61 | @property
62 | def icon(self):
63 | return 'mdi:bell'
64 |
65 | @property
66 | def rotate_angle(self):
67 | """Return the rotate angle."""
68 | return self._attr_rotate_angle
69 |
70 | @property
71 | def action_duration(self):
72 | """Return the action duration."""
73 | return self._attr_action_duration
74 |
75 | @property
76 | def rotate_angle_w_hold(self):
77 | """Return the rotate angle with hold"""
78 | return self._attr_rotate_angle_w_hold
79 |
80 | @property
81 | def action_duration_w_hold(self):
82 | """Return the action duration with hold."""
83 | return self._attr_action_duration_w_hold
84 |
85 | @property
86 | def extra_state_attributes(self):
87 | """Return the optional state attributes."""
88 | data = {}
89 |
90 | for prop, attr in PROP_TO_ATTR_BASE.items():
91 | value = getattr(self, prop)
92 | if value is not None:
93 | data[attr] = value
94 |
95 | for prop, attr in PROP_TO_ATTR.items():
96 | value = getattr(self, prop)
97 | if value is not None:
98 | data[attr] = value
99 |
100 | return data
101 |
102 | def convert_res_to_attr(self, res_name, res_value):
103 |
104 | if res_name == "chip_temperature":
105 | return round(float(res_value), 1)
106 | if res_name == "fw_ver":
107 | return res_value
108 | if res_name == "lqi":
109 | return int(res_value)
110 | if res_name == "rotate_angle":
111 | return res_value
112 | if res_name == "action_duration":
113 | return res_value
114 | if res_name == "rotate_angle_w_hold":
115 | return res_value
116 | if res_name == "action_duration_w_hold":
117 | return res_value
118 | if res_value != 0 and res_value != "" and res_name == "button":
119 | if res_name == 'vibration' and res_value != '2':
120 | click_type = VIBRATION.get(res_value, 'unkown')
121 | if "button" in res_name:
122 | click_type = BUTTON.get(res_value, 'unkown')
123 | if "cube" in res_name:
124 | click_type = CUBE.get(res_value, 'unkown')
125 |
126 | # repeat event from Aqara integration
127 | self.hass.bus.fire('xiaomi_aqara.click', {
128 | 'entity_id': self.entity_id, 'click_type': click_type
129 | })
130 |
131 | self.schedule_update_ha_state()
132 | return click_type
133 | return super().convert_res_to_attr(res_name, res_value)
--------------------------------------------------------------------------------
/custom_components/aqara_bridge/switch.py:
--------------------------------------------------------------------------------
1 | from homeassistant.components.switch import SwitchEntity
2 |
3 | from .core.aiot_manager import AiotManager, AiotToggleableEntityBase
4 | from .core.const import DOMAIN, HASS_DATA_AIOT_MANAGER, PROP_TO_ATTR_BASE
5 |
6 | TYPE = "switch"
7 |
8 | ATTR_IN_USE = "in_use"
9 |
10 | PROP_TO_ATTR = {
11 | "in_use": ATTR_IN_USE
12 | }
13 |
14 | DATA_KEY = f"{TYPE}.{DOMAIN}"
15 |
16 | async def async_setup_entry(hass, config_entry, async_add_entities):
17 | manager: AiotManager = hass.data[DOMAIN][HASS_DATA_AIOT_MANAGER]
18 | cls_entities = {
19 | "default": AiotSwitchEntity
20 | }
21 | await manager.async_add_entities(
22 | config_entry, TYPE, cls_entities, async_add_entities
23 | )
24 |
25 |
26 | class AiotSwitchEntity(AiotToggleableEntityBase, SwitchEntity):
27 | _attr_in_use = None
28 |
29 | def __init__(self, hass, device, res_params, channel=None, **kwargs):
30 | AiotToggleableEntityBase.__init__(
31 | self, hass, device, res_params, TYPE, channel, **kwargs
32 | )
33 |
34 | @property
35 | def icon(self):
36 | """return icon."""
37 | return 'mdi:power-socket'
38 |
39 | @property
40 | def in_use(self):
41 | """Return the plug detection."""
42 | return self._attr_in_use
43 |
44 | @property
45 | def extra_state_attributes(self):
46 | """Return the optional state attributes."""
47 | data = {}
48 |
49 | for prop, attr in PROP_TO_ATTR_BASE.items():
50 | value = getattr(self, prop)
51 | if value is not None:
52 | data[attr] = value
53 |
54 | for prop, attr in PROP_TO_ATTR.items():
55 | value = getattr(self, prop)
56 | if value is not None:
57 | data[attr] = value
58 |
59 | return data
60 |
61 | def convert_res_to_attr(self, res_name, res_value):
62 | if res_name == "toggle" or res_name == "decoupled":
63 | return res_value == "1"
64 | if res_name == "energy":
65 | return round(float(res_value) / 1000.0, 3)
66 | if res_name == "chip_temperature":
67 | return round(float(res_value), 1)
68 | if res_name == "fw_ver":
69 | return res_value
70 | if res_name == "lqi":
71 | return int(res_value)
72 | if res_name == "in_use":
73 | return res_value == "1"
74 | return super().convert_res_to_attr(res_name, res_value)
75 |
--------------------------------------------------------------------------------
/custom_components/aqara_bridge/translations/en.json:
--------------------------------------------------------------------------------
1 | {
2 | "config": {
3 | "abort": {
4 |
5 | },
6 | "error": {
7 |
8 | },
9 | "flow_title": "{name}",
10 | "step": {
11 | "get_auth_code": {
12 | "data": {
13 | "field_country_code": "Cloud Server Country/Region",
14 | "field_account": "Aqara Home Account (Cellular Number/ Email)",
15 | "field_refresh_token": "Refresh Token for Developer"
16 | },
17 | "description": "Use Aqara Home registered account to login.",
18 | "title": "Login Aqara Cloud Server"
19 | },
20 | "get_token": {
21 | "data": {
22 | "field_auth_code": "Verification Code via SMS or Email"
23 | },
24 | "description": "Please input Verification Code",
25 | "title": "Login Aqara Cloud Server"
26 | },
27 | "select_devices": {
28 | "data": {
29 | "field_selected_devices": "Select devices"
30 | },
31 | "description": "Choose the devices from list.",
32 | "title": "Choose the devices"
33 | }
34 | }
35 | },
36 | "options": {
37 | "step": {
38 | "init": {
39 | "title": "Bridge Config",
40 | "data": {
41 | "stats": "Stats",
42 | "debug": "Debug"
43 | }
44 | }
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/custom_components/aqara_bridge/translations/zh-Hans.json:
--------------------------------------------------------------------------------
1 | {
2 | "config": {
3 | "abort": {
4 |
5 | },
6 | "error": {
7 |
8 | },
9 | "flow_title": "{name}",
10 | "step": {
11 | "get_auth_code": {
12 | "data": {
13 | "field_country_code": "云服务国家/地区",
14 | "field_account": "Aqara Home账号(手机号)",
15 | "field_refresh_token": "刷新令牌(开发用)"
16 | },
17 | "description": "使用在Aqara Home注册的账号进行登录。",
18 | "title": "登录到Aqara云服务平台"
19 | },
20 | "get_token": {
21 | "data": {
22 | "field_auth_code": "短信验证码"
23 | },
24 | "description": "请输入短信验证码。",
25 | "title": "登录到Aqara云服务平台"
26 | },
27 | "select_devices": {
28 | "data": {
29 | "field_selected_devices": "选择设备"
30 | },
31 | "description": "从列表中选择需要绑定的设备。",
32 | "title": "从Aqara云服务平台绑定设备"
33 | }
34 | }
35 | },
36 | "options": {
37 | "step": {
38 | "init": {
39 | "title": "Bridge \u914d\u7f6e",
40 | "data": {
41 | "stats": "Stats",
42 | "debug": "\u9664\u9519"
43 | }
44 | }
45 | }
46 | }
47 | }
--------------------------------------------------------------------------------
/custom_components/aqara_bridge/translations/zh-Hant.json:
--------------------------------------------------------------------------------
1 | {
2 | "config": {
3 | "abort": {
4 |
5 | },
6 | "error": {
7 |
8 | },
9 | "flow_title": "{name}",
10 | "step": {
11 | "get_auth_code": {
12 | "data": {
13 | "field_country_code": "\u96f2\u670d\u52d9\u570b\u5bb6/\u5730\u5340",
14 | "field_account": "Aqara Home \u5e33\u865f\uff08\u624b\u6a5f\u865f\u78bc\uff09/ \u96fb\u5b50\u90f5\u4ef6",
15 | "field_refresh_token": "\u5237\u65b0\u4ee4\u724c\uff08\u958b\u767c\u7528\uff09"
16 | },
17 | "description": "\u4f7f\u7528\u5728 Aqara Home \u8a3b\u518a\u7684\u5e33\u865f\u9032\u884c\u767b\u5165\u3002",
18 | "title": "\u767b\u5165\u5230 Aqara \u96f2\u670d\u52d9\u5e73\u53f0"
19 | },
20 | "get_token": {
21 | "data": {
22 | "field_auth_code": "\u7c21\u8a0a\u9a57\u8b49\u78bc"
23 | },
24 | "description": "\u8acb\u8f38\u5165\u7c21\u8a0a\u9a57\u8b49\u78bc\u3002",
25 | "title": "\u767b\u5165\u5230 Aqara \u96f2\u670d\u52d9\u5e73\u53f0"
26 | },
27 | "select_devices": {
28 | "data": {
29 | "field_selected_devices": "\u9078\u64c7\u8a2d\u5099"
30 | },
31 | "description": "\u5f9e\u6e05\u55ae\u4e2d\u9078\u64c7\u9700\u8981\u7d81\u5b9a\u7684\u8a2d\u5099\u3002",
32 | "title": "\u5f9e Aqara \u96f2\u670d\u52d9\u5e73\u53f0\u7d81\u5b9a\u8a2d\u5099"
33 | }
34 | }
35 | },
36 | "options": {
37 | "step": {
38 | "init": {
39 | "title": "Bridge \u8a2d\u5b9a",
40 | "data": {
41 | "stats": "Stats",
42 | "debug": "\u9664\u932F"
43 | }
44 | }
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/hacs.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Aqara Bridge",
3 | "render_readme": true,
4 | "domain": "aqara_bridge",
5 | "documentation": "https://github.com/niceboygithub/AqaraBridge",
6 | "issue_tracker": "https://github.com/niceboygithub/AqaraBridge/issues"
7 | }
8 |
--------------------------------------------------------------------------------