├── 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 | [![version](https://img.shields.io/github/manifest-json/v/niceboygithub/AqaraBridge?filename=custom_components%2Faqara_bridge%2Fmanifest.json)](https://github.com/niceboygithub/aqara_bridge/releases/latest) [![stars](https://img.shields.io/github/stars/niceboygithub/AqaraBridge)](https://github.com/niceboygithub/aqara_bridge/stargazers) [![issues](https://img.shields.io/github/issues/niceboygithub/AqaraBridge)](https://github.com/niceboygithub/AqaraBridge/issues) [![hacs](https://img.shields.io/badge/HACS-Default-orange.svg)](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 | --------------------------------------------------------------------------------