diff --git a/.github/workflows/validation.yaml b/.github/workflows/validation.yaml new file mode 100644 index 0000000..5779236 --- /dev/null +++ b/.github/workflows/validation.yaml @@ -0,0 +1,16 @@ +name: Validate with hassfest and HACS + +on: + push: + pull_request: + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: home-assistant/actions/hassfest@master + - uses: hacs/action@main + with: + category: integration + ignore: integration_manifest diff --git a/README.md b/README.md index a8376eb..5e28cb1 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ 此项目是Home Assistant平台[DS-AIR](https://www.daikin-china.com.cn/newha/products/4/19/DS-AIR/)以及[金制空气](https://www.daikin-china.com.cn/newha/products/4/19/jzkq/)自定义组件的实现 -支持的网关设备型号为DTA117B611/DTA117C611,其他网关的支持情况未知 +支持的网关设备型号为 DTA117B611、DTA117C611,其他网关的支持情况未知。(DTA117D611 可直接选择 DTA117C611) # 支持设备 @@ -19,9 +19,22 @@ # 接入方法 -1. 将项目ha-air目录部署到自定义组件目录,一般路径为```~/.homeassistant/custom_components/``` - 或使用hacs载入自定义存储库,设置URL```https://github.com/mypal/ha-dsair``` ,类别 ```集成``` -2. 本集成已支持ha可视化配置,在配置-集成-添加集成中选择```DS-AIR``` ,依次填入网关IP、端口号、设备型号提交即可 +## 安装 +- 方法一:将项目 `ds_air` 目录直接拷贝到 `/config/custom_components/` 目录下 + +- 方法二:点击此按钮添加 HACS 自定义存储库 + + [![Open your Home Assistant instance and open a repository inside the Home Assistant Community Store.](https://my.home-assistant.io/badges/hacs_repository.svg)](https://my.home-assistant.io/redirect/hacs_repository/?owner=mypal&repository=ha-dsair&category=integration) + + 然后点击右下角 DOWNLOAD 安装 + +## 配置 + +- 方法一:在`配置-集成-添加集成`中选择`DS-AIR` + +- 方法二:直接点击此按钮 [![Open your Home Assistant instance and start setting up a new integration.](https://my.home-assistant.io/badges/config_flow_start.svg)](https://my.home-assistant.io/redirect/config_flow_start/?domain=ds_air) + +然后依次填入网关IP、端口号、设备型号提交即可 # 开发过程 diff --git a/custom_components/ds_air/__init__.py b/custom_components/ds_air/__init__.py index 34ce21c..4a45f0e 100644 --- a/custom_components/ds_air/__init__.py +++ b/custom_components/ds_air/__init__.py @@ -1,52 +1,41 @@ -""" -Platform for DS-AIR of Daikin +"""Platform for DS-AIR of Daikin + https://www.daikin-china.com.cn/newha/products/4/19/DS-AIR/ """ + import logging from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST, CONF_PORT, CONF_SCAN_INTERVAL +from homeassistant.const import CONF_HOST, CONF_PORT, CONF_SCAN_INTERVAL, Platform from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceEntry -from .hass_inst import GetHass -from .const import CONF_GW, DEFAULT_HOST, DEFAULT_PORT, DEFAULT_GW, DOMAIN -from .ds_air_service.config import Config +from .const import CONF_GW, DEFAULT_GW, DOMAIN +from .ds_air_service import Config, Service _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["climate", "sensor"] +PLATFORMS = [ + Platform.CLIMATE, + Platform.SENSOR, +] -def _log(s: str): - s = str(s) - for i in s.split("\n"): - _LOGGER.debug(i) -def setup(hass, config): - hass.data[DOMAIN] = {} - GetHass.set_hass(hass) - return True - - -async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry -): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) host = entry.data[CONF_HOST] port = entry.data[CONF_PORT] gw = entry.data[CONF_GW] scan_interval = entry.data[CONF_SCAN_INTERVAL] - _log(f"{host}:{port} {gw} {scan_interval}") - - hass.data[DOMAIN][CONF_HOST] = host - hass.data[DOMAIN][CONF_PORT] = port - hass.data[DOMAIN][CONF_GW] = gw - hass.data[DOMAIN][CONF_SCAN_INTERVAL] = scan_interval + _LOGGER.debug("%s:%s %s %s", host, port, gw, scan_interval) - Config.is_c611 = gw == DEFAULT_GW + config = Config() + config.is_c611 = gw == DEFAULT_GW - from .ds_air_service.service import Service - await hass.async_add_executor_job(Service.init, host, port, scan_interval) + service = Service() + hass.data[DOMAIN][entry.entry_id] = service + await hass.async_add_executor_job(service.init, host, port, scan_interval, config) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(update_listener)) @@ -55,15 +44,26 @@ async def async_setup_entry( async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - if hass.data[DOMAIN].get("listener") is not None: - hass.data[DOMAIN].get("listener")() + service: Service = hass.data[DOMAIN].pop(entry.entry_id) + + if service.state_change_listener is not None: + service.state_change_listener() + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - from .ds_air_service.service import Service - Service.destroy() + if not unload_ok: + return False + + service.destroy() - return unload_ok + return True -async def update_listener(hass: HomeAssistant, entry: ConfigEntry): +async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: await hass.config_entries.async_reload(entry.entry_id) + + +async def async_remove_config_entry_device( + hass: HomeAssistant, config_entry: ConfigEntry, device_entry: DeviceEntry +) -> bool: + # reference: https://developers.home-assistant.io/docs/device_registry_index/#removing-devices return True diff --git a/custom_components/ds_air/climate.py b/custom_components/ds_air/climate.py index 2c5e9fb..6d10990 100644 --- a/custom_components/ds_air/climate.py +++ b/custom_components/ds_air/climate.py @@ -1,65 +1,80 @@ -""" -Daikin platform that offers climate devices. +"""Daikin platform that offers climate devices. For more details about this platform, please refer to the documentation https://home-assistant.io/components/demo/ """ import logging -from typing import Optional, List import voluptuous as vol + from homeassistant.components.climate import ( PLATFORM_SCHEMA, + PRESET_COMFORT, + PRESET_NONE, + PRESET_SLEEP, ClimateEntity, ClimateEntityFeature, - HVACMode, HVACAction, - PRESET_NONE, PRESET_SLEEP, PRESET_COMFORT, - FAN_AUTO, FAN_LOW, FAN_MEDIUM, FAN_HIGH + HVACAction, + HVACMode, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import MAJOR_VERSION, MINOR_VERSION, UnitOfTemperature, ATTR_TEMPERATURE, CONF_HOST, CONF_PORT -from homeassistant.core import HomeAssistant, Event +from homeassistant.const import ( + ATTR_TEMPERATURE, + CONF_HOST, + CONF_PORT, + MAJOR_VERSION, + MINOR_VERSION, + PRECISION_TENTHS, + UnitOfTemperature, +) +from homeassistant.core import Event, HomeAssistant from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_state_change_event -from .const import DOMAIN -from .ds_air_service.config import Config -from .ds_air_service.ctrl_enum import EnumControl -from .ds_air_service.dao import AirCon, AirConStatus -from .ds_air_service.display import display +from .const import ( + AIR_FLOW_NAME_LIST, + DOMAIN, + FAN_DIRECTION_LIST, + MANUFACTURER, + get_action_name, + get_air_flow_enum, + get_air_flow_name, + get_fan_direction_enum, + get_fan_direction_name, + get_mode_name, +) +from .ds_air_service import AirCon, AirConStatus, EnumControl, Service, display -_SUPPORT_FLAGS = ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE | ClimateEntityFeature.PRESET_MODE -# | ClimateEntityFeature.SWING_MODE | ClimateEntityFeature.TARGET_HUMIDITY +_SUPPORT_FLAGS = ( + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.FAN_MODE + | ClimateEntityFeature.PRESET_MODE +) if (MAJOR_VERSION, MINOR_VERSION) >= (2024, 2): _SUPPORT_FLAGS |= ClimateEntityFeature.TURN_ON | ClimateEntityFeature.TURN_OFF -FAN_LIST = [ FAN_LOW, '稍弱', FAN_MEDIUM, '稍强', FAN_HIGH, FAN_AUTO] -SWING_LIST = ['➡️', '↘️', '⬇️', '↙️', '⬅️', '↔️', '🔄'] - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_HOST): cv.string, - vol.Optional(CONF_PORT): cv.port -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + {vol.Optional(CONF_HOST): cv.string, vol.Optional(CONF_PORT): cv.port} +) _LOGGER = logging.getLogger(__name__) + def _log(s: str): s = str(s) for i in s.split("\n"): _LOGGER.debug(i) + async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the climate devices.""" - - from .ds_air_service.service import Service - climates = [] - for aircon in Service.get_aircons(): - climates.append(DsAir(aircon)) + service: Service = hass.data[DOMAIN][entry.entry_id] + climates = [DsAir(service, aircon) for aircon in service.get_aircons()] async_add_entities(climates) link = entry.options.get("link") sensor_temp_map: dict[str, list[DsAir]] = {} @@ -67,7 +82,9 @@ async def async_setup_entry( if link is not None: for i in link: climate_name = i.get("climate") - if climate := next(c for c in climates if c.name == climate_name): + if climate := next( + c for c in climates if c._device_info.alias == climate_name + ): if temp_entity_id := i.get("sensor_temp"): sensor_temp_map.setdefault(temp_entity_id, []).append(climate) climate.linked_temp_entity_id = temp_entity_id @@ -84,31 +101,54 @@ async def listener(event: Event): for climate in sensor_humi_map[sensor_id]: climate.update_cur_humi(event.data.get("new_state").state) - remove_listener = async_track_state_change_event(hass, list(sensor_temp_map.keys()) + list(sensor_humi_map.keys()), listener) - hass.data[DOMAIN]["listener"] = remove_listener + remove_listener = async_track_state_change_event( + hass, list(sensor_temp_map.keys()) + list(sensor_humi_map.keys()), listener + ) + service.state_change_listener = remove_listener class DsAir(ClimateEntity): """Representation of a Daikin climate device.""" - _enable_turn_on_off_backwards_compatibility = False # used in 2024.2~2024.12 - - def __init__(self, aircon: AirCon): - _log('create aircon:') + # Entity Properties + _attr_has_entity_name: bool = True + _attr_name: str | None = None + _attr_should_poll: bool = False + + # Climate Properties + _attr_fan_modes: list[str] | None = AIR_FLOW_NAME_LIST + # _attr_max_humidity: float = 3 + _attr_max_temp: float = 32 + # _attr_min_humidity: float = 1 + _attr_min_temp: float = 16 + _attr_precision: float = PRECISION_TENTHS + _attr_swing_modes: list[str] | None = FAN_DIRECTION_LIST[1:] + _attr_target_temperature_high: float | None = None + _attr_target_temperature_low: float | None = None + _attr_target_temperature_step: float | None = 0.5 + _attr_temperature_unit: str = UnitOfTemperature.CELSIUS + + _enable_turn_on_off_backwards_compatibility: bool = False # used in 2024.2~2024.12 + + def __init__(self, service: Service, aircon: AirCon): + _log("create aircon:") _log(str(aircon.__dict__)) _log(str(aircon.status.__dict__)) """Initialize the climate device.""" - self._name = aircon.alias + self.service = service self._device_info = aircon - self._unique_id = aircon.unique_id + self._attr_unique_id = aircon.unique_id self.linked_temp_entity_id: str | None = None self.linked_humi_entity_id: str | None = None self._link_cur_temp = False - self._link_cur_humi = False - self._cur_temp = None - self._cur_humi = None - from .ds_air_service.service import Service - Service.register_status_hook(aircon, self._status_change_hook) + + service.register_status_hook(aircon, self._status_change_hook) + + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, self.unique_id)}, + name=aircon.alias if "空调" in aircon.alias else f"{aircon.alias} 空调", + manufacturer=MANUFACTURER, + ) async def async_added_to_hass(self) -> None: if self.linked_temp_entity_id: @@ -119,16 +159,16 @@ async def async_added_to_hass(self) -> None: self.update_cur_humi(state.state) def _status_change_hook(self, **kwargs): - _log('hook:') - if kwargs.get('aircon') is not None: - aircon: AirCon = kwargs['aircon'] + _log("hook:") + if kwargs.get("aircon") is not None: + aircon: AirCon = kwargs["aircon"] aircon.status = self._device_info.status self._device_info = aircon _log(display(self._device_info)) - if kwargs.get('status') is not None: + if kwargs.get("status") is not None: status: AirConStatus = self._device_info.status - new_status: AirConStatus = kwargs['status'] + new_status: AirConStatus = kwargs["status"] if new_status.mode is not None: status.mode = new_status.mode if new_status.switch is not None: @@ -150,65 +190,44 @@ def _status_change_hook(self, **kwargs): _log(display(self._device_info.status)) self.schedule_update_ha_state() - def update_cur_temp(self, value): + def update_cur_temp(self, value: str | None) -> None: self._link_cur_temp = value is not None try: - self._cur_temp = float(value) + self._attr_current_temperature = float(value) except ValueError: - """Ignore""" + self._attr_current_temperature = None self.schedule_update_ha_state() - def update_cur_humi(self, value): - self._link_cur_humi = value is not None + def update_cur_humi(self, value: str | None) -> None: try: - self._cur_humi = int(float(value)) + self._attr_current_humidity = int(float(value)) except ValueError: - """Ignore""" + self._attr_current_humidity = None self.schedule_update_ha_state() @property - def should_poll(self): - """Return the polling state.""" - return False - - @property - def name(self): - """Return the name of the climate device.""" - return self._name - - @property - def temperature_unit(self): - """Return the unit of measurement.""" - return UnitOfTemperature.CELSIUS - - @property - def target_humidity(self): + def target_humidity(self) -> float | None: """Return the humidity we try to reach.""" return self._device_info.status.humidity.value @property - def hvac_action(self): - """Return current operation ie. heat, cool, idle.""" + def hvac_action(self) -> HVACAction | None: + """Return the current running hvac operation if supported.""" if self._device_info.status.switch == EnumControl.Switch.OFF: return HVACAction.OFF - else: - return EnumControl.get_action_name(self._device_info.status.mode.value) + return get_action_name(self._device_info.status.mode.value) @property - def hvac_mode(self) -> str: - """Return hvac operation ie. heat, cool mode. - - Need to be one of HVAC_MODE_*. - """ + def hvac_mode(self) -> HVACMode | None: + """Return hvac operation ie. heat, cool mode.""" if self._device_info.status.switch == EnumControl.Switch.OFF: return HVACMode.OFF - else: - return EnumControl.get_mode_name(self._device_info.status.mode.value) + return get_mode_name(self._device_info.status.mode.value) @property - def hvac_modes(self): - """Return the list of supported features.""" - li = [] + def hvac_modes(self) -> list[HVACMode]: + """Return the list of available hvac operation modes.""" + li: list[HVACMode] = [] aircon = self._device_info if aircon.cool_mode: li.append(HVACMode.COOL) @@ -224,62 +243,36 @@ def hvac_modes(self): return li @property - def current_temperature(self): + def current_temperature(self) -> float | None: """Return the current temperature.""" if self._link_cur_temp: - return self._cur_temp - else: - if Config.is_c611: - return None - else: - return self._device_info.status.current_temp / 10 + return self._attr_current_temperature + if self._device_info.config.is_c611: + return None + return self._device_info.status.current_temp / 10 @property - def target_temperature(self): + def target_temperature(self) -> float | None: """Return the temperature we try to reach.""" return self._device_info.status.setted_temp / 10 @property - def target_temperature_step(self): - """Return the supported step of target temperature.""" - return 0.5 - - @property - def target_temperature_high(self): - """Return the highbound target temperature we try to reach.""" - return None - - @property - def target_temperature_low(self): - """Return the lowbound target temperature we try to reach.""" - return None - - @property - def current_humidity(self): - """Return the current humidity.""" - if self._link_cur_humi: - return self._cur_humi - else: - return None - - @property - def preset_mode(self) -> Optional[str]: + def preset_mode(self) -> str | None: """Return the current preset mode, e.g., home, away, temp. - Requires SUPPORT_PRESET_MODE. + Requires ClimateEntityFeature.PRESET_MODE. """ if self._device_info.status.mode == EnumControl.Mode.SLEEP: return PRESET_SLEEP - elif self._device_info.status.mode == EnumControl.Mode.RELAX: + if self._device_info.status.mode == EnumControl.Mode.RELAX: return PRESET_COMFORT - else: - return PRESET_NONE + return PRESET_NONE @property - def preset_modes(self) -> Optional[List[str]]: + def preset_modes(self) -> list[str] | None: """Return a list of available preset modes. - Requires SUPPORT_PRESET_MODE. + Requires ClimateEntityFeature.PRESET_MODE. """ result = [] aircon = self._device_info @@ -291,74 +284,62 @@ def preset_modes(self) -> Optional[List[str]]: return result @property - def is_aux_heat(self): - """Return true if aux heat is on.""" - return None + def fan_mode(self) -> str | None: + """Return the fan setting. - @property - def fan_mode(self): - """Return the fan setting.""" - return EnumControl.get_air_flow_name(self._device_info.status.air_flow.value) - - @property - def fan_modes(self) -> Optional[List[str]]: - """Return the list of available fan modes. - - Requires SUPPORT_FAN_MODE. + Requires ClimateEntityFeature.FAN_MODE. """ - return FAN_LIST - - @property - def swing_mode(self): - """Return the swing setting.""" - return EnumControl.get_fan_direction_name(self._device_info.status.fan_direction1.value) + return get_air_flow_name(self._device_info.status.air_flow.value) @property - def swing_modes(self) -> Optional[List[str]]: - """Return the list of available swing modes. + def swing_mode(self) -> str | None: + """Return the swing setting. - Requires SUPPORT_SWING_MODE. + Requires ClimateEntityFeature.SWING_MODE. """ - return SWING_LIST + return get_fan_direction_name(self._device_info.status.fan_direction1.value) - def set_temperature(self, **kwargs): + def set_temperature(self, **kwargs) -> None: """Set new target temperatures.""" - if kwargs.get(ATTR_TEMPERATURE) is not None: + if (temperate := kwargs.get(ATTR_TEMPERATURE)) is not None: status = self._device_info.status - new_status = AirConStatus() - if status.switch == EnumControl.Switch.ON \ - and status.mode not in [EnumControl.Mode.VENTILATION, EnumControl.Mode.MOREDRY]: - status.setted_temp = round(kwargs.get(ATTR_TEMPERATURE) * 10.0) - new_status.setted_temp = round(kwargs.get(ATTR_TEMPERATURE) * 10.0) - from .ds_air_service.service import Service - Service.control(self._device_info, new_status) - self.schedule_update_ha_state() - - def set_humidity(self, humidity): + if status.switch == EnumControl.Switch.ON and status.mode not in [ + EnumControl.Mode.VENTILATION, + EnumControl.Mode.MOREDRY, + ]: + new_status = AirConStatus() + status.setted_temp = round(temperate * 10.0) + new_status.setted_temp = round(temperate * 10.0) + self.service.control(self._device_info, new_status) + self.schedule_update_ha_state() + + def set_humidity(self, humidity: int) -> None: """Set new humidity level.""" status = self._device_info.status - new_status = AirConStatus() - if status.switch == EnumControl.Switch.ON \ - and status.mode in [EnumControl.Mode.RELAX, EnumControl.Mode.SLEEP]: + if status.switch == EnumControl.Switch.ON and status.mode in [ + EnumControl.Mode.RELAX, + EnumControl.Mode.SLEEP, + ]: + new_status = AirConStatus() status.humidity = EnumControl.Humidity(humidity) new_status.humidity = EnumControl.Humidity(humidity) - from .ds_air_service.service import Service - Service.control(self._device_info, new_status) - self.schedule_update_ha_state() + self.service.control(self._device_info, new_status) + self.schedule_update_ha_state() - def set_fan_mode(self, fan_mode): - """Set new fan mode.""" + def set_fan_mode(self, fan_mode: str) -> None: + """Set new target fan mode.""" status = self._device_info.status - new_status = AirConStatus() - if status.switch == EnumControl.Switch.ON \ - and status.mode not in [EnumControl.Mode.MOREDRY, EnumControl.Mode.SLEEP]: - status.air_flow = EnumControl.get_air_flow_enum(fan_mode) - new_status.air_flow = EnumControl.get_air_flow_enum(fan_mode) - from .ds_air_service.service import Service - Service.control(self._device_info, new_status) - self.schedule_update_ha_state() + if status.switch == EnumControl.Switch.ON and status.mode not in [ + EnumControl.Mode.MOREDRY, + EnumControl.Mode.SLEEP, + ]: + new_status = AirConStatus() + status.air_flow = get_air_flow_enum(fan_mode) + new_status.air_flow = get_air_flow_enum(fan_mode) + self.service.control(self._device_info, new_status) + self.schedule_update_ha_state() - def set_hvac_mode(self, hvac_mode: str) -> None: + def set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set new target hvac mode.""" aircon = self._device_info status = aircon.status @@ -366,8 +347,7 @@ def set_hvac_mode(self, hvac_mode: str) -> None: if hvac_mode == HVACMode.OFF: status.switch = EnumControl.Switch.OFF new_status.switch = EnumControl.Switch.OFF - from .ds_air_service.service import Service - Service.control(self._device_info, new_status) + self.service.control(self._device_info, new_status) else: status.switch = EnumControl.Switch.ON new_status.switch = EnumControl.Switch.ON @@ -398,24 +378,23 @@ def set_hvac_mode(self, hvac_mode: str) -> None: mode = m.SLEEP status.mode = mode new_status.mode = mode - from .ds_air_service.service import Service - Service.control(self._device_info, new_status) + self.service.control(self._device_info, new_status) self.schedule_update_ha_state() - def set_swing_mode(self, swing_mode): + def set_swing_mode(self, swing_mode: str) -> None: """Set new swing mode.""" status = self._device_info.status - new_status = AirConStatus() if status.switch == EnumControl.Switch.ON: + new_status = AirConStatus() status.fan_direction1 = self._device_info.status.fan_direction1 new_status.fan_direction1 = self._device_info.status.fan_direction1 - status.fan_direction2 = EnumControl.get_fan_direction_enum(swing_mode) - new_status.fan_direction2 = EnumControl.get_fan_direction_enum(swing_mode) - from .ds_air_service.service import Service - Service.control(self._device_info, new_status) - self.schedule_update_ha_state() + status.fan_direction2 = get_fan_direction_enum(swing_mode) + new_status.fan_direction2 = get_fan_direction_enum(swing_mode) + self.service.control(self._device_info, new_status) + self.schedule_update_ha_state() def set_preset_mode(self, preset_mode: str) -> None: + """Set new preset mode.""" aircon = self._device_info status = aircon.status new_status = AirConStatus() @@ -428,60 +407,22 @@ def set_preset_mode(self, preset_mode: str) -> None: mode = m.RELAX else: mode = m.COLD - else: - if preset_mode == PRESET_SLEEP: - mode = m.SLEEP - elif preset_mode == PRESET_COMFORT: - mode = m.RELAX + elif preset_mode == PRESET_SLEEP: + mode = m.SLEEP + elif preset_mode == PRESET_COMFORT: + mode = m.RELAX status.mode = mode new_status.mode = mode - from .ds_air_service.service import Service - Service.control(self._device_info, new_status) + self.service.control(self._device_info, new_status) self.schedule_update_ha_state() - def turn_aux_heat_on(self) -> None: - pass - - def turn_aux_heat_off(self) -> None: - pass - @property - def supported_features(self) -> int: + def supported_features(self) -> ClimateEntityFeature: """Return the list of supported features.""" - SUPPORT_FLAGS = _SUPPORT_FLAGS + flags = _SUPPORT_FLAGS aircon = self._device_info - if self._device_info.status.fan_direction1.value > 0: - SUPPORT_FLAGS = SUPPORT_FLAGS | ClimateEntityFeature.SWING_MODE + if aircon.status.fan_direction1.value > 0: + flags |= ClimateEntityFeature.SWING_MODE if aircon.relax_mode: - SUPPORT_FLAGS = SUPPORT_FLAGS | ClimateEntityFeature.TARGET_HUMIDITY - return SUPPORT_FLAGS - - @property - def min_temp(self): - """Return the minimum temperature.""" - return 16 - - @property - def max_temp(self): - """Return the maximum temperature.""" - return 32 - - @property - def min_humidity(self): - return 1 - - @property - def max_humidity(self): - return 3 - - @property - def device_info(self) -> Optional[DeviceInfo]: - return { - "identifiers": {(DOMAIN, self.unique_id)}, - "name": "空调%s" % self._name, - "manufacturer": "Daikin Industries, Ltd." - } - - @property - def unique_id(self) -> Optional[str]: - return self._unique_id + flags |= ClimateEntityFeature.TARGET_HUMIDITY + return flags diff --git a/custom_components/ds_air/config_flow.py b/custom_components/ds_air/config_flow.py index d70f9d9..b609a04 100644 --- a/custom_components/ds_air/config_flow.py +++ b/custom_components/ds_air/config_flow.py @@ -4,9 +4,9 @@ from typing import Any import voluptuous as vol -from homeassistant import config_entries + from homeassistant.components.sensor import SensorDeviceClass -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_FRIENDLY_NAME, @@ -15,12 +15,10 @@ CONF_SCAN_INTERVAL, CONF_SENSORS, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from .const import CONF_GW, DEFAULT_GW, DEFAULT_HOST, DEFAULT_PORT, DOMAIN, GW_LIST -from .ds_air_service.service import Service -from .hass_inst import GetHass _LOGGER = logging.getLogger(__name__) @@ -31,41 +29,36 @@ def _log(s: str) -> None: _LOGGER.debug(i) -class DsAirFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class DsAirFlowHandler(ConfigFlow, domain=DOMAIN): VERSION = 1 def __init__(self): - self.host = None - self.port = None - self.gw = None - self.sensor_check = {} self.user_input = {} - async def async_step_user(self, user_input: dict[str, Any] | None = None) -> FlowResult: - if self._async_current_entries(): - return self.async_abort(reason="single_instance_allowed") - + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: errors = {} if user_input is not None: self.user_input.update(user_input) - if user_input.get(CONF_SENSORS) == False or user_input.get("temp") is not None: + if not user_input.get(CONF_SENSORS) or user_input.get("temp") is not None: return self.async_create_entry(title="金制空气", data=self.user_input) - else: - return self.async_show_form( - step_id="user", - data_schema=vol.Schema( - { - vol.Required("temp", default=True): bool, - vol.Required("humidity", default=True): bool, - vol.Required("pm25", default=True): bool, - vol.Required("co2", default=True): bool, - vol.Required("tvoc", default=True): bool, - vol.Required("voc", default=False): bool, - vol.Required("hcho", default=False): bool, - } - ), - errors=errors, - ) + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required("temp", default=True): bool, + vol.Required("humidity", default=True): bool, + vol.Required("pm25", default=True): bool, + vol.Required("co2", default=True): bool, + vol.Required("tvoc", default=True): bool, + vol.Required("voc", default=False): bool, + vol.Required("hcho", default=False): bool, + } + ), + errors=errors, + ) return self.async_show_form( step_id="user", @@ -83,99 +76,117 @@ async def async_step_user(self, user_input: dict[str, Any] | None = None) -> Flo @staticmethod @callback - def async_get_options_flow(config_entry: ConfigEntry) -> DsAirOptionsFlowHandler: + def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow: """Options callback for DS-AIR.""" return DsAirOptionsFlowHandler(config_entry) -class DsAirOptionsFlowHandler(config_entries.OptionsFlow): +class DsAirOptionsFlowHandler(OptionsFlow): """Config flow options for integration""" - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + def __init__(self, config_entry: ConfigEntry) -> None: """Initialize options flow.""" self.config_entry = config_entry self._config_data = [] - hass: HomeAssistant = GetHass.get_hash() - self._climates = list(map(lambda state: state.alias, Service.get_aircons())) - sensors = hass.states.async_all("sensor") + self._climates: list[str] = [] # set in async_step_init + self._len: int = 0 # set in async_step_init + self._sensors_temp: dict[str, str] = {} + self._sensors_humi: dict[str, str] = {} + self._cur = -1 + self.user_input = {} + + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Manage the options.""" + service = self.hass.data[DOMAIN][self.config_entry.entry_id] + self._climates = [state.alias for state in service.get_aircons()] + self._len = len(self._climates) + + sensors = self.hass.states.async_all("sensor") self._sensors_temp = { - None: 'None', + None: "None", **{ state.entity_id: f"{state.attributes.get(ATTR_FRIENDLY_NAME, state.entity_id)} ({state.entity_id})" for state in sensors - if state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE - } + if state.attributes.get(ATTR_DEVICE_CLASS) + == SensorDeviceClass.TEMPERATURE + }, } self._sensors_humi = { - None: 'None', + None: "None", **{ state.entity_id: f"{state.attributes.get(ATTR_FRIENDLY_NAME, state.entity_id)} ({state.entity_id})" for state in sensors if state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.HUMIDITY - } + }, } - self._len = len(self._climates) - self._cur = -1 - self.host = CONF_HOST - self.port = CONF_PORT - self.gw = CONF_GW - self.sensor_check = CONF_SENSORS - self.user_input = {} - async def async_step_init(self, user_input: dict[str, Any] | None = None) -> FlowResult: - """Manage the options.""" return self.async_show_menu( step_id="init", menu_options=["adjust_config", "bind_sensors"], ) - async def async_step_adjust_config(self, user_input: dict[str, Any] | None = None) -> FlowResult: + async def async_step_adjust_config( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: errors = {} if user_input is not None: self.user_input.update(user_input) if self.user_input.get("_invaild"): self.user_input["_invaild"] = False - self.hass.config_entries.async_update_entry(self.config_entry, data=self.user_input) + self.hass.config_entries.async_update_entry( + self.config_entry, data=self.user_input + ) return self.async_create_entry(title="", data={}) else: self.user_input["_invaild"] = True - if CONF_SENSORS: - return self.async_show_form( - step_id="adjust_config", - data_schema=vol.Schema( - { - vol.Required(CONF_HOST, default=self.config_entry.data[CONF_HOST]): str, - vol.Required(CONF_PORT, default=self.config_entry.data[CONF_PORT]): int, - vol.Required(CONF_GW, default=self.config_entry.data[CONF_GW]): vol.In(GW_LIST), - vol.Required(CONF_SCAN_INTERVAL, default=self.config_entry.data[CONF_SCAN_INTERVAL]): int, - vol.Required(CONF_SENSORS, default=True): bool, - vol.Required("temp", default=self.config_entry.data["temp"]): bool, - vol.Required("humidity", default=self.config_entry.data["humidity"]): bool, - vol.Required("pm25", default=self.config_entry.data["pm25"]): bool, - vol.Required("co2", default=self.config_entry.data["co2"]): bool, - vol.Required("tvoc", default=self.config_entry.data["tvoc"]): bool, - vol.Required("voc", default=self.config_entry.data["voc"]): bool, - vol.Required("hcho", default=self.config_entry.data["hcho"]): bool, - } - ), - errors=errors, - ) - else: - return self.async_show_form( - step_id="adjust_config", - data_schema=vol.Schema( - { - vol.Required(CONF_HOST, default=self.config_entry.data[CONF_HOST]): str, - vol.Required(CONF_PORT, default=self.config_entry.data[CONF_PORT]): int, - vol.Required(CONF_GW, default=self.config_entry.data[CONF_GW]): vol.In(GW_LIST), - vol.Required(CONF_SCAN_INTERVAL, default=self.config_entry.data[CONF_SCAN_INTERVAL]): int, - vol.Required(CONF_SENSORS, default=False): bool, - } - ), - errors=errors, - ) - - async def async_step_bind_sensors(self, user_input: dict[str, Any] | None = None) -> FlowResult: + data = self.config_entry.data + # if CONF_SENSORS: + return self.async_show_form( + step_id="adjust_config", + data_schema=vol.Schema( + { + vol.Required(CONF_HOST, default=data[CONF_HOST]): str, + vol.Required(CONF_PORT, default=data[CONF_PORT]): int, + vol.Required(CONF_GW, default=data[CONF_GW]): vol.In(GW_LIST), + vol.Required( + CONF_SCAN_INTERVAL, default=data[CONF_SCAN_INTERVAL] + ): int, + vol.Required(CONF_SENSORS, default=True): bool, + vol.Required("temp", default=data["temp"]): bool, + vol.Required("humidity", default=data["humidity"]): bool, + vol.Required("pm25", default=data["pm25"]): bool, + vol.Required("co2", default=data["co2"]): bool, + vol.Required("tvoc", default=data["tvoc"]): bool, + vol.Required("voc", default=data["voc"]): bool, + vol.Required("hcho", default=data["hcho"]): bool, + } + ), + errors=errors, + ) + # else: + # return self.async_show_form( + # step_id="adjust_config", + # data_schema=vol.Schema( + # { + # vol.Required(CONF_HOST, default=data[CONF_HOST]): str, + # vol.Required(CONF_PORT, default=data[CONF_PORT]): int, + # vol.Required(CONF_GW, default=data[CONF_GW]): vol.In( + # GW_LIST + # ), + # vol.Required( + # CONF_SCAN_INTERVAL, default=data[CONF_SCAN_INTERVAL] + # ): int, + # vol.Required(CONF_SENSORS, default=False): bool, + # } + # ), + # errors=errors, + # ) + + async def async_step_bind_sensors( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle bind flow.""" if self._len == 0: return self.async_show_form(step_id="empty", last_step=False) @@ -192,7 +203,9 @@ async def async_step_bind_sensors(self, user_input: dict[str, Any] | None = None return self.async_create_entry(title="", data={"link": self._config_data}) cur_climate: str = self._climates[self._cur] cur_links = self.config_entry.options.get("link", []) - cur_link = next((link for link in cur_links if link["climate"] == cur_climate), None) + cur_link = next( + (link for link in cur_links if link["climate"] == cur_climate), None + ) cur_sensor_temp = cur_link.get("sensor_temp") if cur_link else None cur_sensor_humi = cur_link.get("sensor_humi") if cur_link else None return self.async_show_form( @@ -200,12 +213,18 @@ async def async_step_bind_sensors(self, user_input: dict[str, Any] | None = None data_schema=vol.Schema( { vol.Required("climate", default=cur_climate): vol.In([cur_climate]), - vol.Optional("sensor_temp", default=cur_sensor_temp): vol.In(self._sensors_temp), - vol.Optional("sensor_humi", default=cur_sensor_humi): vol.In(self._sensors_humi), + vol.Optional("sensor_temp", default=cur_sensor_temp): vol.In( + self._sensors_temp + ), + vol.Optional("sensor_humi", default=cur_sensor_humi): vol.In( + self._sensors_humi + ), } ), ) - async def async_step_empty(self, user_input: dict[str, Any] | None = None) -> FlowResult: + async def async_step_empty( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """No AC found.""" return await self.async_step_init(user_input) diff --git a/custom_components/ds_air/const.py b/custom_components/ds_air/const.py index 87e9218..63eb105 100644 --- a/custom_components/ds_air/const.py +++ b/custom_components/ds_air/const.py @@ -1,8 +1,13 @@ -from homeassistant.const import UnitOfTemperature, PERCENTAGE, CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, \ - CONCENTRATION_PARTS_PER_MILLION, CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER -from homeassistant.components.sensor import SensorDeviceClass +from homeassistant.components.climate import ( + FAN_AUTO, + FAN_HIGH, + FAN_LOW, + FAN_MEDIUM, + HVACAction, + HVACMode, +) -from .ds_air_service.ctrl_enum import EnumSensor +from .ds_air_service import EnumControl DOMAIN = "ds_air" CONF_GW = "gw" @@ -10,12 +15,63 @@ DEFAULT_PORT = 8008 DEFAULT_GW = "DTA117C611" GW_LIST = ["DTA117C611", "DTA117B611"] -SENSOR_TYPES = { - "temp": [UnitOfTemperature.CELSIUS, None, SensorDeviceClass.TEMPERATURE, 10], - "humidity": [PERCENTAGE, None, SensorDeviceClass.HUMIDITY, 10], - "pm25": [CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, None, SensorDeviceClass.PM25, 1], - "co2": [CONCENTRATION_PARTS_PER_MILLION, None, SensorDeviceClass.CO2, 1], - "tvoc": [CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, None, SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS, 0.1], - "voc": [None, None, SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS_PARTS, EnumSensor.Voc], - "hcho": [CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER, None, None, 100], -} + +MANUFACTURER = "Daikin Industries, Ltd." + + +_MODE_NAME_LIST = [ + HVACMode.COOL, + HVACMode.DRY, + HVACMode.FAN_ONLY, + HVACMode.AUTO, + HVACMode.HEAT, + HVACMode.DRY, + HVACMode.AUTO, + HVACMode.AUTO, + HVACMode.HEAT, + HVACMode.DRY, +] + + +def get_mode_name(idx: int) -> HVACMode: + return _MODE_NAME_LIST[idx] + + +_MODE_ACTION_LIST = [ + HVACAction.COOLING, + HVACAction.DRYING, + HVACAction.FAN, + None, + HVACAction.HEATING, + HVACAction.DRYING, + None, + None, + HVACAction.PREHEATING, + HVACAction.DRYING, +] + + +def get_action_name(idx: int) -> HVACAction | None: + return _MODE_ACTION_LIST[idx] + + +AIR_FLOW_NAME_LIST = [FAN_LOW, "稍弱", FAN_MEDIUM, "稍强", FAN_HIGH, FAN_AUTO] + + +def get_air_flow_name(idx: int) -> str: + return AIR_FLOW_NAME_LIST[idx] + + +def get_air_flow_enum(name: str) -> EnumControl.AirFlow: + return EnumControl.AirFlow(AIR_FLOW_NAME_LIST.index(name)) + + +FAN_DIRECTION_LIST = [None, "➡️", "↘️", "⬇️", "↙️", "⬅️", "↔️", "🔄"] + + +def get_fan_direction_name(idx: int) -> str: + return FAN_DIRECTION_LIST[idx] + + +def get_fan_direction_enum(name: str) -> EnumControl.FanDirection: + return EnumControl.FanDirection(FAN_DIRECTION_LIST.index(name)) diff --git a/custom_components/ds_air/descriptions.py b/custom_components/ds_air/descriptions.py new file mode 100644 index 0000000..758c44a --- /dev/null +++ b/custom_components/ds_air/descriptions.py @@ -0,0 +1,71 @@ +from collections.abc import Callable +from dataclasses import dataclass +from typing import Any + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.const import ( + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER, + CONCENTRATION_PARTS_PER_MILLION, + MAJOR_VERSION, + PERCENTAGE, + UnitOfTemperature, +) + +FROZEN = MAJOR_VERSION >= 2024 + + +@dataclass(frozen=FROZEN, kw_only=True) +class DsSensorEntityDescription(SensorEntityDescription): + has_entity_name: bool = True + state_class: SensorStateClass = SensorStateClass.MEASUREMENT + value_fn: Callable[[Any], Any] | None = lambda x: x + + +SENSOR_DESCRIPTORS = { + "temp": DsSensorEntityDescription( + key="temp", + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + value_fn=lambda x: x / 10, + ), + "humidity": DsSensorEntityDescription( + key="humidity", + native_unit_of_measurement=PERCENTAGE, + device_class=SensorDeviceClass.HUMIDITY, + value_fn=lambda x: x / 10, + ), + "pm25": DsSensorEntityDescription( + key="pm25", + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + device_class=SensorDeviceClass.PM25, + ), + "co2": DsSensorEntityDescription( + key="co2", + native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, + device_class=SensorDeviceClass.CO2, + ), + "tvoc": DsSensorEntityDescription( + key="tvoc", + name="TVOC", + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS, + suggested_display_precision=0, + value_fn=lambda x: x * 10, + ), + "voc": DsSensorEntityDescription( + key="voc", + device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS_PARTS, + value_fn=lambda x: str(x), # EnumSensor.Voc + ), + "hcho": DsSensorEntityDescription( + key="hcho", + name="HCHO", + native_unit_of_measurement=CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER, + value_fn=lambda x: x / 100, + ), +} diff --git a/custom_components/ds_air/ds_air_service/__init__.py b/custom_components/ds_air/ds_air_service/__init__.py new file mode 100644 index 0000000..9db2f5e --- /dev/null +++ b/custom_components/ds_air/ds_air_service/__init__.py @@ -0,0 +1,16 @@ +from .config import Config +from .ctrl_enum import EnumControl +from .dao import UNINITIALIZED_VALUE, AirCon, AirConStatus, Sensor +from .display import display +from .service import Service + +__all__ = [ + "Config", + "EnumControl", + "UNINITIALIZED_VALUE", + "AirCon", + "AirConStatus", + "Sensor", + "display", + "Service", +] diff --git a/custom_components/ds_air/ds_air_service/base_bean.py b/custom_components/ds_air/ds_air_service/base_bean.py index 9f4ffe7..daf5cb6 100644 --- a/custom_components/ds_air/ds_air_service/base_bean.py +++ b/custom_components/ds_air/ds_air_service/base_bean.py @@ -1,4 +1,4 @@ -from .ctrl_enum import EnumDevice, EnumCmdType +from .ctrl_enum import EnumCmdType, EnumDevice class BaseBean: diff --git a/custom_components/ds_air/ds_air_service/ctrl_enum.py b/custom_components/ds_air/ds_air_service/ctrl_enum.py index 87b8220..33d50b4 100644 --- a/custom_components/ds_air/ds_air_service/ctrl_enum.py +++ b/custom_components/ds_air/ds_air_service/ctrl_enum.py @@ -1,14 +1,5 @@ from enum import Enum, IntEnum -from homeassistant.components.climate.const import ( - FAN_AUTO, - FAN_HIGH, - FAN_LOW, - FAN_MEDIUM, - HVACAction, - HVACMode, -) - class EnumCmdType(IntEnum): # 返回指令 @@ -229,48 +220,6 @@ class EnumSwitch(IntEnum): OFF = 2 -"""EnumControl""" - - -class AirFlow(IntEnum): - SUPER_WEAK = 0 - WEAK = 1 - MIDDLE = 2 - STRONG = 3 - SUPER_STRONG = 4 - AUTO = 5 - - -#_AIR_FLOW_NAME_LIST = ['最弱', '稍弱', '中等', '稍强', '最强', '自动'] -_AIR_FLOW_NAME_LIST = [FAN_LOW, '稍弱', FAN_MEDIUM, '稍强', FAN_HIGH, FAN_AUTO] - -class Breathe(IntEnum): - CLOSE = 0 - WEAK = 1 - STRONG = 2 - - -class FanDirection(IntEnum): - INVALID = 0 - P0 = 1 # 最右 最上 - P1 = 2 - P2 = 3 - P3 = 4 - P4 = 5 # 最左 最下 - AUTO = 6 - SWING = 7 - - -_FAN_DIRECTION_LIST = ['INVALID', '➡️', '↘️', '⬇️', '↙️', '⬅️', '↔️', '🔄'] - - -class Humidity(IntEnum): - CLOSE = 0 - STEP1 = 1 - STEP2 = 2 - STEP3 = 3 - - class FreshAirHumidification(IntEnum): OFF = 0 FRESH_AIR = 1 @@ -284,89 +233,68 @@ class ThreeDFresh(IntEnum): AUTO = 3 -class Mode(IntEnum): - COLD = 0 - DRY = 1 - VENTILATION = 2 - AUTO = 3 - HEAT = 4 - AUTODRY = 5 - RELAX = 6 - SLEEP = 7 - PREHEAT = 8 - MOREDRY = 9 - -# Legacy Mode Mapping -#_MODE_NAME_LIST = [HVACMode.COOL, HVACMode.DRY, HVACMode.FAN_ONLY, HVACMode.AUTO, HVACMode.HEAT, -# HVACMode.DRY, HVACMode.AUTO, HVACMode.HEAT_COOL, HVACMode.HEAT, HVACMode.DRY] - -_MODE_NAME_LIST = [HVACMode.COOL, HVACMode.DRY, HVACMode.FAN_ONLY, HVACMode.AUTO, HVACMode.HEAT, - HVACMode.DRY, HVACMode.AUTO, HVACMode.AUTO, HVACMode.HEAT, HVACMode.DRY] -_MODE_ACTION_LIST = [HVACAction.COOLING, HVACAction.DRYING, HVACAction.FAN, None, HVACAction.HEATING, - HVACAction.DRYING, None, None, HVACAction.PREHEATING, HVACAction.DRYING] - -class Switch(IntEnum): - OFF = 0 - ON = 1 - - -class Type(IntEnum): - SWITCH = 1 # 0 - MODE = 2 # 1 - AIR_FLOW = 4 # 2 - CURRENT_TEMP = 8 - FRESH_AIR_HUMIDIFICATION = 8 # 3 - SETTED_TEMP = 16 # 4 - FAN_DIRECTION = 32 # 5 - HUMIDITY = 64 # 6 - BREATHE = 128 # 7 - FAN_DIRECTION_FB = 254 # 8 - FAN_DIRECTION_LR = 255 # 9 - SCENE_STATE = 253 # 10 - - class EnumControl: - Switch = Switch - AirFlow = AirFlow - Breathe = Breathe - FanDirection = FanDirection - Humidity = Humidity - Mode = Mode - Type = Type - - @staticmethod - def get_mode_name(idx): - return _MODE_NAME_LIST[idx] - - def get_action_name(idx): - return _MODE_ACTION_LIST[idx] - - @staticmethod - def get_mode_enum(name): - return Mode(_MODE_NAME_LIST.index(name)) - - @staticmethod - def get_air_flow_name(idx): - return _AIR_FLOW_NAME_LIST[idx] - - @staticmethod - def get_air_flow_enum(name): - return AirFlow(_AIR_FLOW_NAME_LIST.index(name)) - - @staticmethod - def get_fan_direction_name(idx): - return _FAN_DIRECTION_LIST[idx] - - @staticmethod - def get_fan_direction_enum(name): - return FanDirection(_FAN_DIRECTION_LIST.index(name)) + class Switch(IntEnum): + OFF = 0 + ON = 1 + + class AirFlow(IntEnum): + SUPER_WEAK = 0 + WEAK = 1 + MIDDLE = 2 + STRONG = 3 + SUPER_STRONG = 4 + AUTO = 5 + + class Breathe(IntEnum): + CLOSE = 0 + WEAK = 1 + STRONG = 2 + + class FanDirection(IntEnum): + INVALID = 0 + P0 = 1 # 最右 最上 + P1 = 2 + P2 = 3 + P3 = 4 + P4 = 5 # 最左 最下 + AUTO = 6 + SWING = 7 + + class Humidity(IntEnum): + CLOSE = 0 + STEP1 = 1 + STEP2 = 2 + STEP3 = 3 + + class Mode(IntEnum): + COLD = 0 + DRY = 1 + VENTILATION = 2 + AUTO = 3 + HEAT = 4 + AUTODRY = 5 + RELAX = 6 + SLEEP = 7 + PREHEAT = 8 + MOREDRY = 9 + + class Type(IntEnum): + SWITCH = 1 # 0 + MODE = 2 # 1 + AIR_FLOW = 4 # 2 + CURRENT_TEMP = 8 + FRESH_AIR_HUMIDIFICATION = 8 # 3 + SETTED_TEMP = 16 # 4 + FAN_DIRECTION = 32 # 5 + HUMIDITY = 64 # 6 + BREATHE = 128 # 7 + FAN_DIRECTION_FB = 254 # 8 + FAN_DIRECTION_LR = 255 # 9 + SCENE_STATE = 253 # 10 class EnumSensor: - class LinkState(IntEnum): - NO_LINKED = 0 - YES_LINKED = 1 - class Voc(IntEnum): STEP_1 = 1 STEP_2 = 2 @@ -377,11 +305,11 @@ class Voc(IntEnum): def __str__(self): if self.value == EnumSensor.Voc.STEP_UNUSE: return "不可用" - elif self.value == EnumSensor.Voc.STEP_1: + if self.value == EnumSensor.Voc.STEP_1: return "优" - elif self.value == EnumSensor.Voc.STEP_2: + if self.value == EnumSensor.Voc.STEP_2: return "低" - elif self.value == EnumSensor.Voc.STEP_3: + if self.value == EnumSensor.Voc.STEP_3: return "中" - elif self.value == EnumSensor.Voc.STEP_4: + if self.value == EnumSensor.Voc.STEP_4: return "高" diff --git a/custom_components/ds_air/ds_air_service/dao.py b/custom_components/ds_air/ds_air_service/dao.py index 38d6350..ceb429f 100644 --- a/custom_components/ds_air/ds_air_service/dao.py +++ b/custom_components/ds_air/ds_air_service/dao.py @@ -1,7 +1,14 @@ import time -from typing import Optional -from .ctrl_enum import EnumOutDoorRunCond, EnumFanDirection, EnumFanVolume, EnumSwitch, EnumControl, EnumDevice +from .config import Config +from .ctrl_enum import ( + EnumControl, + EnumDevice, + EnumFanDirection, + EnumFanVolume, + EnumOutDoorRunCond, + EnumSwitch, +) class Device: @@ -15,67 +22,68 @@ def __init__(self): @property def unique_id(self): + # todo 需要加上所属网关 return "daikin_%d_%d" % (self.room_id, self.unit_id) -def _nothing(): - """do nothing""" - - class AirConStatus: - def __init__(self, current_temp: int = None, setted_temp: int = None, - switch: EnumControl.Switch = None, - air_flow: EnumControl.AirFlow = None, - breathe: EnumControl.Breathe = None, - fan_direction1: EnumControl.FanDirection = None, - fan_direction2: EnumControl.FanDirection = None, - humidity: EnumControl.Humidity = None, - mode: EnumControl.Mode = None): - self.current_temp = current_temp # type: int - self.setted_temp = setted_temp # type: int - self.switch = switch # type: EnumControl.Switch - self.air_flow = air_flow # type: EnumControl.AirFlow - self.breathe = breathe # type: EnumControl.Breathe - self.fan_direction1 = fan_direction1 # type: EnumControl.FanDirection - self.fan_direction2 = fan_direction2 # type: EnumControl.FanDirection - self.humidity = humidity # type: EnumControl.Humidity - self.mode = mode # type: EnumControl.Mode + def __init__( + self, + current_temp: int | None = None, + setted_temp: int | None = None, + switch: EnumControl.Switch | None = None, + air_flow: EnumControl.AirFlow | None = None, + breathe: EnumControl.Breathe | None = None, + fan_direction1: EnumControl.FanDirection | None = None, + fan_direction2: EnumControl.FanDirection | None = None, + humidity: EnumControl.Humidity | None = None, + mode: EnumControl.Mode | None = None, + ): + self.current_temp: int | None = current_temp + self.setted_temp: int | None = setted_temp + self.switch: EnumControl.Switch | None = switch + self.air_flow: EnumControl.AirFlow | None = air_flow + self.breathe: EnumControl.Breathe | None = breathe + self.fan_direction1: EnumControl.FanDirection | None = fan_direction1 + self.fan_direction2: EnumControl.FanDirection | None = fan_direction2 + self.humidity: EnumControl.Humidity | None = humidity + self.mode: EnumControl.Mode | None = mode class AirCon(Device): - def __init__(self): + def __init__(self, config: Config): super().__init__() - self.auto_dry_mode = 0 # type: int - self.auto_mode = 0 # type: int - self.bath_room = False # type: bool - self.new_air_con = False # type: bool - self.cool_mode = 0 # type: int - self.dry_mode = 0 # type: int - self.fan_dire_auto = False # type: bool - self.fan_direction1 = EnumFanDirection.FIX # type: EnumFanDirection - self.fan_direction2 = EnumFanDirection.FIX # type: EnumFanDirection - self.fan_volume = EnumFanVolume.FIX # type: EnumFanVolume - self.fan_volume_auto = False # type: bool - self.temp_set = False # type: bool - self.hum_fresh_air_allow = False # type: bool - self.three_d_fresh_allow = False # type: bool - self.heat_mode = 0 # type: int - self.more_dry_mode = 0 # type: int - self.out_door_run_cond = EnumOutDoorRunCond.VENT # type: EnumOutDoorRunCond - self.pre_heat_mode = 0 # type: int - self.relax_mode = 0 # type: int - self.sleep_mode = 0 # type: int - self.ventilation_mode = 0 # type: int - self.status = AirConStatus() # type: AirConStatus + self.config = config + self.auto_dry_mode: int = 0 + self.auto_mode: int = 0 + self.bath_room: bool = False + self.new_air_con: bool = False + self.cool_mode: int = 0 + self.dry_mode: int = 0 + self.fan_dire_auto: bool = False + self.fan_direction1: EnumFanDirection = EnumFanDirection.FIX + self.fan_direction2: EnumFanDirection = EnumFanDirection.FIX + self.fan_volume: EnumFanVolume = EnumFanVolume.FIX + self.fan_volume_auto: bool = False + self.temp_set: bool = False + self.hum_fresh_air_allow: bool = False + self.three_d_fresh_allow: bool = False + self.heat_mode: int = 0 + self.more_dry_mode: int = 0 + self.out_door_run_cond: EnumOutDoorRunCond = EnumOutDoorRunCond.VENT + self.pre_heat_mode: int = 0 + self.relax_mode: int = 0 + self.sleep_mode: int = 0 + self.ventilation_mode: int = 0 + self.status: AirConStatus = AirConStatus() def get_device_by_aircon(aircon: AirCon): if aircon.new_air_con: return EnumDevice.NEWAIRCON - elif aircon.bath_room: + if aircon.bath_room: return EnumDevice.BATHROOM - else: - return EnumDevice.AIRCON + return EnumDevice.AIRCON class Geothermic(Device): @@ -85,7 +93,7 @@ class Geothermic(Device): class Ventilation(Device): def __init__(self): Device.__init__(self) - self.is_small_vam = False # type: bool + self.is_small_vam: bool = False class HD(Device): @@ -94,19 +102,44 @@ def __init__(self): self.switch: EnumSwitch -STATUS_ATTR = ["mac", "type1", "type2", "start_time", "stop_time", "sensor_type", "temp", "humidity", "pm25", "co2", - "voc", "tvoc", "hcho", "switch_on_off", "temp_upper", "temp_lower", "humidity_upper", - "humidity_lower", "pm25_upper", "pm25_lower", "co2_upper", "co2_lower", "voc_lower", "tvoc_upper", - "hcho_upper", "connected", "sleep_mode_count", "time_millis"] +STATUS_ATTR = [ + "mac", + "type1", + "type2", + "start_time", + "stop_time", + "sensor_type", + "temp", + "humidity", + "pm25", + "co2", + "voc", + "tvoc", + "hcho", + "switch_on_off", + "temp_upper", + "temp_lower", + "humidity_upper", + "humidity_lower", + "pm25_upper", + "pm25_lower", + "co2_upper", + "co2_lower", + "voc_lower", + "tvoc_upper", + "hcho_upper", + "connected", + "sleep_mode_count", + "time_millis", +] UNINITIALIZED_VALUE = -1000 class Sensor(Device): - def __init__(self): Device.__init__(self) - self.mac: str = '' + self.mac: str = "" self.type1: int = 0 self.type2: int = 0 self.start_time: int = 0 @@ -139,13 +172,13 @@ def __init__(self): class Room: def __init__(self): self.air_con = None - self.alias = '' # type: str - self.geothermic = None # type: Optional[Geothermic] - self.hd = None # type: Optional[HD] - self.hd_room = False # type: bool - self.sensor_room = False # type: bool - self.icon = '' # type: str - self.id = 0 # type: int - self.name = '' # type: str - self.type = 0 # type: int - self.ventilation = Ventilation() # type: Optional[Ventilation] + self.alias: str = "" + self.geothermic: Geothermic | None = None + self.hd: HD | None = None + self.hd_room: bool = False + self.sensor_room: bool = False + self.icon: str = "" + self.id: int = 0 + self.name: str = "" + self.type: int = 0 + self.ventilation: Ventilation | None = Ventilation() diff --git a/custom_components/ds_air/ds_air_service/decoder.py b/custom_components/ds_air/ds_air_service/decoder.py index a7ef310..ef3802a 100644 --- a/custom_components/ds_air/ds_air_service/decoder.py +++ b/custom_components/ds_air/ds_air_service/decoder.py @@ -1,32 +1,81 @@ +from __future__ import annotations + import struct -import typing +from typing import TYPE_CHECKING from .base_bean import BaseBean from .config import Config -from .ctrl_enum import EnumDevice, EnumCmdType, EnumFanDirection, EnumOutDoorRunCond, EnumFanVolume, EnumControl, \ - EnumSensor, FreshAirHumidification, ThreeDFresh -from .dao import Room, AirCon, Geothermic, Ventilation, HD, Device, AirConStatus, get_device_by_aircon, Sensor, \ - UNINITIALIZED_VALUE -from .param import GetRoomInfoParam, AirConRecommendedIndoorTempParam, AirConCapabilityQueryParam, \ - AirConQueryStatusParam, Sensor2InfoParam - - -def decoder(b): +from .ctrl_enum import ( + EnumCmdType, + EnumControl, + EnumDevice, + EnumFanDirection, + EnumFanVolume, + EnumOutDoorRunCond, + EnumSensor, + FreshAirHumidification, + ThreeDFresh, +) +from .dao import ( + HD, + UNINITIALIZED_VALUE, + AirCon, + AirConStatus, + Device, + Geothermic, + Room, + Sensor, + Ventilation, + get_device_by_aircon, +) +from .param import ( + AirConCapabilityQueryParam, + AirConQueryStatusParam, + AirConRecommendedIndoorTempParam, + GetRoomInfoParam, + Sensor2InfoParam, +) + +if TYPE_CHECKING: + from .service import Service + + +def decoder(b: bytes, config: Config): if b[0] != 2: return None, None - length = struct.unpack(' None: + """Do nothing""" - def do(self): - """do nothing""" + def do(self, service: Service) -> None: + """Do nothing""" class HeartbeatResult(BaseResult): @@ -151,8 +204,8 @@ class AckResult(BaseResult): def __init__(self, cmd_id: int, target: EnumDevice): BaseResult.__init__(self, cmd_id, target, EnumCmdType.SYS_ACK) - def load_bytes(self, b): - Config.is_new_version = struct.unpack(' None: + config.is_new_version = struct.unpack(" None: data = Decode(b) self._mode = data.read1() count = data.read1() @@ -253,9 +306,8 @@ def load_bytes(self, b): self._sensors.append(sensor) count = count - 1 - def do(self): - from .service import Service - Service.set_sensors_status(self._sensors) + def do(self, service: Service) -> None: + service.set_sensors_status(self._sensors) @property def count(self): @@ -284,8 +336,8 @@ def __init__(self, cmd_id: int, target: EnumDevice): self._cmdId = None self._code = None - def load_bytes(self, b): - self._cmdId, self._code = struct.unpack(' None: + self._cmdId, self._code = struct.unpack(" None: + self._time = struct.unpack(" None: + dev_id, room, unit = struct.unpack(" None: + ( + self._condition, + self._humidity, + self._temp, + self._wind_dire, + self._wind_speed, + ) = struct.unpack(" None: + self._status = struct.unpack(" None: + self._status = struct.unpack(" None: ver_flag = 1 d = Decode(b) self._count = d.read2() room_count = d.read1() - for i in range(room_count): + for _i in range(room_count): room = Room() room.id = d.read2() if self.subbody_ver == 1: @@ -427,31 +484,35 @@ def load_bytes(self, b): length = d.read1() room.icon = d.read_utf(length) unit_count = d.read2() - for j in range(unit_count): + for _j in range(unit_count): device = EnumDevice((8, d.read4())) device_count = d.read2() for unit_id in range(device_count): - if EnumDevice.AIRCON == device or EnumDevice.NEWAIRCON == device or EnumDevice.BATHROOM == device: - dev = AirCon() + if device in ( + EnumDevice.AIRCON, + EnumDevice.NEWAIRCON, + EnumDevice.BATHROOM, + ): + dev = AirCon(config) room.air_con = dev - dev.new_air_con = EnumDevice.NEWAIRCON == device - dev.bath_room = EnumDevice.BATHROOM == device - elif EnumDevice.GEOTHERMIC == device: + dev.new_air_con = device == EnumDevice.NEWAIRCON + dev.bath_room = device == EnumDevice.BATHROOM + elif device == EnumDevice.GEOTHERMIC: dev = Geothermic() room.geothermic = dev - elif EnumDevice.HD == device: + elif device == EnumDevice.HD: dev = HD() self.hds.append(dev) room.hd_room = True room.hd = dev - elif EnumDevice.SENSOR == device: + elif device == EnumDevice.SENSOR: dev = Sensor() self.sensors.append(dev) room.sensor_room = True - elif EnumDevice.VENTILATION == device or EnumDevice.SMALL_VAM == device: + elif device in (EnumDevice.VENTILATION, EnumDevice.SMALL_VAM): dev = Ventilation() room.ventilation = dev - dev.is_small_vam = EnumDevice.SMALL_VAM == device + dev.is_small_vam = device == EnumDevice.SMALL_VAM else: dev = Device() dev.room_id = room.id @@ -465,16 +526,15 @@ def load_bytes(self, b): dev.alias = room.alias self.rooms.append(room) - def do(self): - from .service import Service - Service.set_rooms(self.rooms) - Service.send_msg(AirConRecommendedIndoorTempParam()) - Service.set_sensors(self.sensors) + def do(self, service: Service) -> None: + service.set_rooms(self.rooms) + service.send_msg(AirConRecommendedIndoorTempParam()) + service.set_sensors(self.sensors) aircons = [] new_aircons = [] bathrooms = [] - for room in Service.get_rooms(): + for room in service.get_rooms(): if room.air_con is not None: room.air_con.alias = room.alias if room.air_con.new_air_con: @@ -487,15 +547,15 @@ def do(self): p = AirConCapabilityQueryParam() p.aircons = aircons p.target = EnumDevice.AIRCON - Service.send_msg(p) + service.send_msg(p) p = AirConCapabilityQueryParam() p.aircons = new_aircons p.target = EnumDevice.NEWAIRCON - Service.send_msg(p) + service.send_msg(p) p = AirConCapabilityQueryParam() p.aircons = bathrooms p.target = EnumDevice.BATHROOM - Service.send_msg(p) + service.send_msg(p) @property def count(self): @@ -516,73 +576,75 @@ def sensors(self): class QueryScheduleSettingResult(BaseResult): def __init__(self, cmd_id: int, target: EnumDevice): - BaseResult.__init__(self, cmd_id, target, EnumCmdType.SYS_QUERY_SCHEDULE_SETTING) + BaseResult.__init__( + self, cmd_id, target, EnumCmdType.SYS_QUERY_SCHEDULE_SETTING + ) - def load_bytes(self, b): - """todo""" + def load_bytes(self, b: bytes, config: Config) -> None: + """Todo""" class QueryScheduleIDResult(BaseResult): def __init__(self, cmd_id: int, target: EnumDevice): BaseResult.__init__(self, cmd_id, target, EnumCmdType.SYS_QUERY_SCHEDULE_ID) - def load_bytes(self, b): - """todo""" + def load_bytes(self, b: bytes, config: Config) -> None: + """Todo""" class HandShakeResult(BaseResult): def __init__(self, cmd_id: int, target: EnumDevice): BaseResult.__init__(self, cmd_id, target, EnumCmdType.SYS_HAND_SHAKE) - self._time: str = '' + self._time: str = "" - def load_bytes(self, b): + def load_bytes(self, b: bytes, config: Config) -> None: d = Decode(b) self._time = d.read_utf(14) - def do(self): + def do(self, service: Service) -> None: p = GetRoomInfoParam() - p.room_ids.append(0xffff) - from .service import Service - Service.send_msg(p) - Service.send_msg(Sensor2InfoParam()) + p.room_ids.append(0xFFFF) + + service.send_msg(p) + service.send_msg(Sensor2InfoParam()) class GetGWInfoResult(BaseResult): def __init__(self, cmd_id: int, target: EnumDevice): BaseResult.__init__(self, cmd_id, target, EnumCmdType.SYS_HAND_SHAKE) - self._time: str = '' + self._time: str = "" - def load_bytes(self, b): - """todo""" + def load_bytes(self, b: bytes, config: Config) -> None: + """Todo""" - def do(self): - """todo""" + def do(self, service: Service) -> None: + """Todo""" class CmdTransferResult(BaseResult): def __init__(self, cmd_id: int, target: EnumDevice): BaseResult.__init__(self, cmd_id, target, EnumCmdType.SYS_CMD_TRANSFER) - def load_bytes(self, b): - """todo""" + def load_bytes(self, b: bytes, config: Config) -> None: + """Todo""" class QueryScheduleFinish(BaseResult): def __init__(self, cmd_id: int, target: EnumDevice): BaseResult.__init__(self, cmd_id, target, EnumCmdType.SYS_QUERY_SCHEDULE_FINISH) - def load_bytes(self, b): - """todo""" + def load_bytes(self, b: bytes, config: Config) -> None: + """Todo""" class AirConStatusChangedResult(BaseResult): def __init__(self, cmd_id: int, target: EnumDevice): BaseResult.__init__(self, cmd_id, target, EnumCmdType.STATUS_CHANGED) - self._room = 0 # type: int - self._unit = 0 # type: int - self._status = AirConStatus() # type: AirConStatus + self._room: int = 0 + self._unit: int = 0 + self._status: AirConStatus = AirConStatus() - def load_bytes(self, b): + def load_bytes(self, b: bytes, config: Config) -> None: d = Decode(b) self._room = d.read1() self._unit = d.read1() @@ -598,15 +660,14 @@ def load_bytes(self, b): status.current_temp = d.read2() if flag & EnumControl.Type.SETTED_TEMP: status.setted_temp = d.read2() - if Config.is_new_version: + if config.is_new_version: if flag & EnumControl.Type.FAN_DIRECTION: direction = d.read1() status.fan_direction1 = EnumControl.FanDirection(direction & 0xF) status.fan_direction2 = EnumControl.FanDirection((direction >> 4) & 0xF) - def do(self): - from .service import Service - Service.update_aircon(self.target, self._room, self._unit, status=self._status) + def do(self, service: Service) -> None: + service.update_aircon(self.target, self._room, self._unit, status=self._status) class AirConQueryStatusResult(BaseResult): @@ -628,7 +689,7 @@ def __init__(self, cmd_id: int, target: EnumDevice): self.fresh_air_humidification = FreshAirHumidification.OFF self.three_d_fresh = ThreeDFresh.CLOSE - def load_bytes(self, b): + def load_bytes(self, b: bytes, config: Config) -> None: d = Decode(b) self.room = d.read1() self.unit = d.read1() @@ -639,7 +700,7 @@ def load_bytes(self, b): self.mode = EnumControl.Mode(d.read1()) if flag >> 2 & 1: self.air_flow = EnumControl.AirFlow(d.read1()) - if Config.is_c611: + if config.is_c611: if flag >> 3 & 1: bt = d.read1() self.hum_allow = bt & 8 == 8 @@ -648,11 +709,11 @@ def load_bytes(self, b): if flag >> 4 & 1: self.setted_temp = d.read2() - if Config.is_new_version: + if config.is_new_version: if flag >> 5 & 1: b = d.read1() - self.fan_direction1 = EnumControl.FanDirection(b & 0xf) - self.fan_direction2 = EnumControl.FanDirection(b >> 4 & 0xf) + self.fan_direction1 = EnumControl.FanDirection(b & 0xF) + self.fan_direction2 = EnumControl.FanDirection(b >> 4 & 0xF) if flag >> 6 & 1: self.humidity = EnumControl.Humidity(d.read1()) if self.target == EnumDevice.BATHROOM: @@ -666,32 +727,41 @@ def load_bytes(self, b): self.current_temp = d.read2() if flag >> 4 & 1: self.setted_temp = d.read2() - if Config.is_new_version: + if config.is_new_version: if flag >> 5 & 1: b = d.read1() - self.fan_direction1 = EnumControl.FanDirection(b & 0xf) - self.fan_direction2 = EnumControl.FanDirection(b >> 4 & 0xf) + self.fan_direction1 = EnumControl.FanDirection(b & 0xF) + self.fan_direction2 = EnumControl.FanDirection(b >> 4 & 0xF) if self.target == EnumDevice.NEWAIRCON: if flag >> 6 & 1: self.humidity = EnumControl.Humidity(d.read1()) - else: - if flag >> 7 & 1: - self.breathe = EnumControl.Breathe(d.read1()) - - def do(self): - from .service import Service - status = AirConStatus(self.current_temp, self.setted_temp, self.switch, self.air_flow, self.breathe, - self.fan_direction1, self.fan_direction2, self.humidity, self.mode) - Service.set_aircon_status(self.target, self.room, self.unit, status) + elif flag >> 7 & 1: + self.breathe = EnumControl.Breathe(d.read1()) + + def do(self, service: Service) -> None: + status = AirConStatus( + self.current_temp, + self.setted_temp, + self.switch, + self.air_flow, + self.breathe, + self.fan_direction1, + self.fan_direction2, + self.humidity, + self.mode, + ) + service.set_aircon_status(self.target, self.room, self.unit, status) class AirConRecommendedIndoorTempResult(BaseResult): def __init__(self, cmd_id: int, target: EnumDevice): - BaseResult.__init__(self, cmd_id, target, EnumCmdType.AIR_RECOMMENDED_INDOOR_TEMP) + BaseResult.__init__( + self, cmd_id, target, EnumCmdType.AIR_RECOMMENDED_INDOOR_TEMP + ) self._temp: int = 0 self._outdoor_temp: int = 0 - def load_bytes(self, b): + def load_bytes(self, b: bytes, config: Config) -> None: d = Decode(b) self._temp = d.read2() self._outdoor_temp = d.read2() @@ -708,16 +778,16 @@ def outdoor_temp(self): class AirConCapabilityQueryResult(BaseResult): def __init__(self, cmd_id: int, target: EnumDevice): BaseResult.__init__(self, cmd_id, target, EnumCmdType.AIR_CAPABILITY_QUERY) - self._air_cons: typing.List[AirCon] = [] + self._air_cons: list[AirCon] = [] - def load_bytes(self, b): + def load_bytes(self, b: bytes, config: Config) -> None: d = Decode(b) room_size = d.read1() - for i in range(room_size): + for _i in range(room_size): room_id = d.read1() unit_size = d.read1() - for j in range(unit_size): - aircon = AirCon() + for _j in range(unit_size): + aircon = AirCon(config) aircon.unit_id = d.read1() aircon.room_id = room_id aircon.new_air_con = self.target == EnumDevice.NEWAIRCON @@ -729,7 +799,7 @@ def load_bytes(self, b): aircon.heat_mode = flag >> 2 & 1 aircon.cool_mode = flag >> 1 & 1 aircon.ventilation_mode = flag & 1 - if Config.is_new_version: + if config.is_new_version: flag = d.read1() if flag & 1: aircon.fan_direction1 = EnumFanDirection.STEP_5 @@ -758,20 +828,20 @@ def load_bytes(self, b): d.read1() self._air_cons.append(aircon) - def do(self): - from .service import Service - if Service.is_ready(): + def do(self, service: Service) -> None: + if service.is_ready(): if len(self._air_cons): for i in self._air_cons: - Service.update_aircon(get_device_by_aircon(i), i.room_id, i.unit_id, aircon=i) + service.update_aircon( + get_device_by_aircon(i), i.room_id, i.unit_id, aircon=i + ) else: for i in self._air_cons: p = AirConQueryStatusParam() p.target = self.target p.device = i - from .service import Service - Service.send_msg(p) - Service.set_device(self.target, self._air_cons) + service.send_msg(p) + service.set_device(self.target, self._air_cons) @property def aircons(self): @@ -782,17 +852,17 @@ class AirConQueryScenarioSettingResult(BaseResult): def __init__(self, cmd_id: int, target: EnumDevice): BaseResult.__init__(self, cmd_id, target, EnumCmdType.QUERY_SCENARIO_SETTING) - def load_bytes(self, b): - """todo""" + def load_bytes(self, b: bytes, config: Config) -> None: + """Todo""" class UnknownResult(BaseResult): def __init__(self, cmd_id: int, target: EnumDevice, cmd_type: EnumCmdType): BaseResult.__init__(self, cmd_id, target, cmd_type) - self._subbody = '' + self._subbody = "" - def load_bytes(self, b): - self._subbody = struct.pack('<' + str(len(b)) + 's', b).hex() + def load_bytes(self, b: bytes, config: Config) -> None: + self._subbody = struct.pack("<" + str(len(b)) + "s", b).hex() @property def subbody(self): diff --git a/custom_components/ds_air/ds_air_service/display.py b/custom_components/ds_air/ds_air_service/display.py index 438a0a2..1facd8c 100644 --- a/custom_components/ds_air/ds_air_service/display.py +++ b/custom_components/ds_air/ds_air_service/display.py @@ -1,22 +1,24 @@ from enum import Enum -def display(o, d='') -> str: - if type(o) == int or type(o) == str or type(o) == bool or type(o) == float: +def display(o, d="") -> str: + if isinstance(o, (int, str, bool, float)): return str(o) - elif isinstance(o, Enum): + + if isinstance(o, Enum): return o.name - elif type(o) == list: - st = '[' + + if isinstance(o, list): + st = "[" for i in range(len(o)): - st += '\n' + d + str(i) + ': ' + display(o[i], d + ' ') - st += ']' - return st - else: - li = dir(o) - st = ("\033[31m%s:\033[0m"%o.__class__.__name__) + ' {' - for i in li: - if (not i.startswith('_')) and (not callable(o.__getattribute__(i))): - st += '\n' + d + i + ': ' + display(o.__getattribute__(i), d + ' ') - st += '}' + st += "\n" + d + str(i) + ": " + display(o[i], d + " ") + st += "]" return st + + li = dir(o) + st = f"\033[31m{o.__class__.__name__}:\033[0m" + " {" + for i in li: + if (not i.startswith("_")) and (not callable(o.__getattribute__(i))): + st += "\n" + d + i + ": " + display(o.__getattribute__(i), d + " ") + st += "}" + return st diff --git a/custom_components/ds_air/ds_air_service/param.py b/custom_components/ds_air/ds_air_service/param.py index 8622778..384e69d 100644 --- a/custom_components/ds_air/ds_air_service/param.py +++ b/custom_components/ds_air/ds_air_service/param.py @@ -1,39 +1,43 @@ import struct -import typing -from typing import Optional -from .config import Config -from .dao import AirCon, Device, get_device_by_aircon, AirConStatus from .base_bean import BaseBean -from .ctrl_enum import EnumCmdType, EnumDevice, EnumControl, EnumFanDirection, EnumFanVolume +from .config import Config +from .ctrl_enum import ( + EnumCmdType, + EnumControl, + EnumDevice, + EnumFanDirection, + EnumFanVolume, +) +from .dao import AirCon, AirConStatus, get_device_by_aircon class Encode: def __init__(self): - self._fmt = '<' + self._fmt = "<" self._len = 0 self._list = [] def write1(self, d): - self._fmt += 'B' + self._fmt += "B" self._len += 1 self._list.append(d) def write2(self, d): - self._fmt += 'H' + self._fmt += "H" self._len += 2 self._list.append(d) def write4(self, d): - self._fmt += 'I' + self._fmt += "I" self._len += 4 self._list.append(d) def writes(self, d): - self._fmt += str(len(d)) + 's' + self._fmt += str(len(d)) + "s" self._len += len(d) - def pack(self, rewrite_length: bool = True): + def pack(self, rewrite_length: bool = True) -> bytes: if rewrite_length: self._list[1] = self._len - 4 return struct.pack(self._fmt, *self._list) @@ -46,15 +50,17 @@ def len(self): class Param(BaseBean): cnt = 0 - def __init__(self, device_type: EnumDevice, cmd_type: EnumCmdType, has_result: bool): + def __init__( + self, device_type: EnumDevice, cmd_type: EnumCmdType, has_result: bool + ): Param.cnt += 1 BaseBean.__init__(self, Param.cnt, device_type, cmd_type) self._has_result = has_result - def generate_subbody(self, s): + def generate_subbody(self, s: Encode, config: Config) -> None: return - def to_string(self): + def to_string(self, config: Config) -> bytes: s = Encode() s.write1(2) # 0 保留字 s.write2(16) # 1~2 长度,不含首尾保留字及长度本身 @@ -67,7 +73,7 @@ def to_string(self): s.write4(self.target.value[1]) # 12~15 设备类型id s.write1(self.need_ack) # 16 是否需要ack s.write2(self.cmd_type.value) # 17~18 命令类型id - self.generate_subbody(s) + self.generate_subbody(s, config) s.write1(3) # 最后一位 保留字 return s.pack() @@ -80,7 +86,7 @@ class HeartbeatParam(Param): def __init__(self): super().__init__(EnumDevice.SYSTEM, EnumCmdType.SYS_ACK, False) - def to_string(self): + def to_string(self, config: Config) -> bytes: s = Encode() s.write1(2) s.write2(0) @@ -106,11 +112,11 @@ def __init__(self): class GetRoomInfoParam(SystemParam): def __init__(self): SystemParam.__init__(self, EnumCmdType.SYS_GET_ROOM_INFO, True) - self._room_ids: typing.List[int] = [] + self._room_ids: list[int] = [] self.type: int = 1 self.subbody_ver: int = 1 - def generate_subbody(self, s): + def generate_subbody(self, s: Encode, config: Config) -> None: s.write1(len(self.room_ids)) for r in self.room_ids: s.write2(r) @@ -128,7 +134,7 @@ def __init__(self): Param.__init__(self, EnumDevice.SENSOR, EnumCmdType.SENSOR2_INFO, True) # self._sensor_type: int = 1 - def generate_subbody(self, s): + def generate_subbody(self, s: Encode, config: Config) -> None: s.write1(255) @@ -140,9 +146,9 @@ def __init__(self, cmd_cype, has_result): class AirConCapabilityQueryParam(AirconParam): def __init__(self): AirconParam.__init__(self, EnumCmdType.AIR_CAPABILITY_QUERY, True) - self._aircons: typing.List[AirCon] = [] + self._aircons: list[AirCon] = [] - def generate_subbody(self, s): + def generate_subbody(self, s: Encode, config: Config) -> None: s.write1(len(self._aircons)) for i in self._aircons: s.write1(i.room_id) @@ -166,9 +172,9 @@ def __init__(self): class AirConQueryStatusParam(AirconParam): def __init__(self): super().__init__(EnumCmdType.QUERY_STATUS, True) - self._device = None # type: Optional[AirCon] + self._device: AirCon | None = None - def generate_subbody(self, s): + def generate_subbody(self, s: Encode, config: Config) -> None: s.write1(self._device.room_id) s.write1(self._device.unit_id) t = EnumControl.Type @@ -177,12 +183,10 @@ def generate_subbody(self, s): if dev is not None: if dev.fan_volume != EnumFanVolume.NO: flag = flag | t.AIR_FLOW - if Config.is_new_version: - if dev.fan_direction1 != EnumFanDirection.FIX and dev.fan_direction2 != EnumFanDirection.FIX: + if config.is_new_version: + if EnumFanDirection.FIX not in (dev.fan_direction1, dev.fan_direction2): flag = flag | t.FAN_DIRECTION - if dev.bath_room: - flag = flag | t.BREATHE - elif dev.three_d_fresh_allow: + if dev.bath_room or dev.three_d_fresh_allow: flag = flag | t.BREATHE flag = flag | t.HUMIDITY if dev.hum_fresh_air_allow: @@ -205,7 +209,7 @@ def __init__(self, aircon: AirCon, new_status: AirConStatus): self._aircon = aircon self._new_status = new_status - def generate_subbody(self, s): + def generate_subbody(self, s: Encode, config: Config) -> None: aircon = self._aircon status = self._new_status s.write1(aircon.room_id) @@ -227,7 +231,7 @@ def generate_subbody(self, s): if status.setted_temp is not None: flag = flag | EnumControl.Type.SETTED_TEMP li.append((2, status.setted_temp)) - if Config.is_new_version: + if config.is_new_version: if self.target != EnumDevice.BATHROOM: if status.fan_direction1 is not None: flag = flag | EnumControl.Type.FAN_DIRECTION diff --git a/custom_components/ds_air/ds_air_service/service.py b/custom_components/ds_air/ds_air_service/service.py index fa38198..90be93c 100644 --- a/custom_components/ds_air/ds_air_service/service.py +++ b/custom_components/ds_air/ds_air_service/service.py @@ -1,34 +1,45 @@ +from __future__ import annotations + +from collections.abc import Callable import logging import socket +from threading import Lock, Thread import time -import typing -from threading import Thread, Lock +from .config import Config from .ctrl_enum import EnumDevice -from .dao import Room, AirCon, AirConStatus, get_device_by_aircon, Sensor, STATUS_ATTR -from .decoder import decoder, BaseResult +from .dao import STATUS_ATTR, AirCon, AirConStatus, Room, Sensor, get_device_by_aircon +from .decoder import BaseResult, decoder from .display import display -from .param import Param, HandShakeParam, HeartbeatParam, AirConControlParam, AirConQueryStatusParam, Sensor2InfoParam +from .param import ( + AirConControlParam, + AirConQueryStatusParam, + HandShakeParam, + HeartbeatParam, + Param, + Sensor2InfoParam, +) _LOGGER = logging.getLogger(__name__) def _log(s: str): s = str(s) - for i in s.split('\n'): + for i in s.split("\n"): _LOGGER.debug(i) class SocketClient: - def __init__(self, host: str, port: int): + def __init__(self, host: str, port: int, service: Service, config: Config): self._host = host self._port = port + self._config = config self._locker = Lock() self._s = None while not self.do_connect(): time.sleep(3) self._ready = True - self._recv_thread = RecvThread(self) + self._recv_thread = RecvThread(self, service) self._recv_thread.start() def destroy(self): @@ -40,29 +51,30 @@ def do_connect(self): self._s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try: self._s.connect((self._host, self._port)) - _log('connected') - return True - except socket.error as exc: - _log('connected error') + except OSError as exc: + _log("connected error") _log(str(exc)) return False + else: + _log("connected") + return True def send(self, p: Param): self._locker.acquire() - _log("send hex: 0x"+p.to_string().hex()) - _log('\033[31msend:\033[0m') + _log("send hex: 0x" + p.to_string(self._config).hex()) + _log("\033[31msend:\033[0m") _log(display(p)) done = False while not done: try: - self._s.sendall(p.to_string()) + self._s.sendall(p.to_string(self._config)) done = True except Exception: time.sleep(3) self.do_connect() self._locker.release() - def recv(self) -> (typing.List[BaseResult], bytes): + def recv(self) -> (list[BaseResult], bytes): res = [] done = False data = None @@ -77,10 +89,10 @@ def recv(self) -> (typing.List[BaseResult], bytes): time.sleep(3) self.do_connect() if data is not None: - _log("recv hex: 0x"+data.hex()) + _log("recv hex: 0x" + data.hex()) while data: try: - r, b = decoder(data) + r, b = decoder(data, self._config) res.append(r) data = b except Exception as e: @@ -90,9 +102,10 @@ def recv(self) -> (typing.List[BaseResult], bytes): class RecvThread(Thread): - def __init__(self, sock: SocketClient): + def __init__(self, sock: SocketClient, service: Service): super().__init__() self._sock = sock + self._service = service self._locker = Lock() self._running = True @@ -103,20 +116,21 @@ def run(self) -> None: while self._running: res = self._sock.recv() for i in res: - _log('\033[31mrecv:\033[0m') + _log("\033[31mrecv:\033[0m") _log(display(i)) self._locker.acquire() try: if i is not None: - i.do() + i.do(self._service) except Exception as e: _log(e) self._locker.release() class HeartBeatThread(Thread): - def __init__(self): + def __init__(self, service: Service): super().__init__() + self.service = service self._running = True def terminate(self): @@ -127,167 +141,160 @@ def run(self) -> None: time.sleep(30) cnt = 0 while self._running: - Service.send_msg(HeartbeatParam()) + self.service.send_msg(HeartbeatParam()) cnt += 1 - if cnt == Service.get_scan_interval(): + if cnt == self.service.get_scan_interval(): _log("poll_status") cnt = 0 - Service.poll_status() + self.service.poll_status() time.sleep(60) class Service: - _socket_client = None # type: SocketClient - _rooms = None # type: typing.List[Room] - _aircons = None # type: typing.List[AirCon] - _new_aircons = None # type: typing.List[AirCon] - _bathrooms = None # type: typing.List[AirCon] - _ready = False # type: bool - _none_stat_dev_cnt = 0 # type: int - _status_hook = [] # type: typing.List[(AirCon, typing.Callable)] - _sensor_hook = [] # type: typing.List[(str, typing.Callable)] - _heartbeat_thread = None - _sensors = [] # type: typing.List[Sensor] - _scan_interval = 5 # type: int - - @staticmethod - def init(host: str, port: int, scan_interval: int): - if Service._ready: + def __init__(self): + self._socket_client: SocketClient = None + self._rooms: list[Room] = None + self._aircons: list[AirCon] = None + self._new_aircons: list[AirCon] = None + self._bathrooms: list[AirCon] = None + self._ready: bool = False + self._none_stat_dev_cnt: int = 0 + self._status_hook: list[(AirCon, Callable)] = [] + self._sensor_hook: list[(str, Callable)] = [] + self._heartbeat_thread = None + self._sensors: list[Sensor] = [] + self._scan_interval: int = 5 + self.state_change_listener: Callable[[], None] | None = None + + def init(self, host: str, port: int, scan_interval: int, config: Config) -> None: + if self._ready: return - Service._scan_interval = scan_interval - Service._socket_client = SocketClient(host, port) - Service._socket_client.send(HandShakeParam()) - Service._heartbeat_thread = HeartBeatThread() - Service._heartbeat_thread.start() - while Service._rooms is None or Service._aircons is None \ - or Service._new_aircons is None or Service._bathrooms is None: + self._scan_interval = scan_interval + self._socket_client = SocketClient(host, port, self, config) + self._socket_client.send(HandShakeParam()) + self._heartbeat_thread = HeartBeatThread(self) + self._heartbeat_thread.start() + while ( + self._rooms is None + or self._aircons is None + or self._new_aircons is None + or self._bathrooms is None + ): time.sleep(1) - for i in Service._aircons: - for j in Service._rooms: + for i in self._aircons: + for j in self._rooms: if i.room_id == j.id: i.alias = j.alias if i.unit_id: i.alias += str(i.unit_id) - for i in Service._new_aircons: - for j in Service._rooms: + for i in self._new_aircons: + for j in self._rooms: if i.room_id == j.id: i.alias = j.alias if i.unit_id: i.alias += str(i.unit_id) - for i in Service._bathrooms: - for j in Service._rooms: + for i in self._bathrooms: + for j in self._rooms: if i.room_id == j.id: i.alias = j.alias if i.unit_id: i.alias += str(i.unit_id) - Service._ready = True - - @staticmethod - def destroy(): - if Service._ready: - Service._heartbeat_thread.terminate() - Service._socket_client.destroy() - Service._socket_client = None - Service._rooms = None - Service._aircons = None - Service._new_aircons = None - Service._bathrooms = None - Service._none_stat_dev_cnt = 0 - Service._status_hook = [] - Service._sensor_hook = [] - Service._heartbeat_thread = None - Service._sensors = [] - Service._ready = False - - @staticmethod - def get_aircons(): + self._ready = True + + def destroy(self) -> None: + if self._ready: + self._heartbeat_thread.terminate() + self._socket_client.destroy() + self._socket_client = None + self._rooms = None + self._aircons = None + self._new_aircons = None + self._bathrooms = None + self._none_stat_dev_cnt = 0 + self._status_hook = [] + self._sensor_hook = [] + self._heartbeat_thread = None + self._sensors = [] + self._ready = False + + def get_aircons(self) -> list[AirCon]: aircons = [] - if Service._new_aircons is not None: - aircons += Service._new_aircons - if Service._aircons is not None: - aircons += Service._aircons - if Service._bathrooms is not None: - aircons += Service._bathrooms + if self._new_aircons is not None: + aircons += self._new_aircons + if self._aircons is not None: + aircons += self._aircons + if self._bathrooms is not None: + aircons += self._bathrooms return aircons - @staticmethod - def control(aircon: AirCon, status: AirConStatus): + def control(self, aircon: AirCon, status: AirConStatus): p = AirConControlParam(aircon, status) - Service.send_msg(p) + self.send_msg(p) - @staticmethod - def register_status_hook(device: AirCon, hook: typing.Callable): - Service._status_hook.append((device, hook)) + def register_status_hook(self, device: AirCon, hook: Callable): + self._status_hook.append((device, hook)) - @staticmethod - def register_sensor_hook(unique_id: str, hook: typing.Callable): - Service._sensor_hook.append((unique_id, hook)) + def register_sensor_hook(self, unique_id: str, hook: Callable): + self._sensor_hook.append((unique_id, hook)) # ----split line---- above for component, below for inner call - @staticmethod - def is_ready() -> bool: - return Service._ready + def is_ready(self) -> bool: + return self._ready - @staticmethod - def send_msg(p: Param): - """send msg to climate gateway""" - Service._socket_client.send(p) + def send_msg(self, p: Param): + """Send msg to climate gateway""" + self._socket_client.send(p) - @staticmethod - def get_rooms(): - return Service._rooms + def get_rooms(self): + return self._rooms - @staticmethod - def set_rooms(v: typing.List[Room]): - Service._rooms = v + def set_rooms(self, v: list[Room]): + self._rooms = v - @staticmethod - def get_sensors(): - return Service._sensors + def get_sensors(self): + return self._sensors - @staticmethod - def set_sensors(sensors): - Service._sensors = sensors + def set_sensors(self, sensors): + self._sensors = sensors - @staticmethod - def set_device(t: EnumDevice, v: typing.List[AirCon]): - Service._none_stat_dev_cnt += len(v) + def set_device(self, t: EnumDevice, v: list[AirCon]): + self._none_stat_dev_cnt += len(v) if t == EnumDevice.AIRCON: - Service._aircons = v + self._aircons = v elif t == EnumDevice.NEWAIRCON: - Service._new_aircons = v + self._new_aircons = v else: - Service._bathrooms = v + self._bathrooms = v - @staticmethod - def set_aircon_status(target: EnumDevice, room: int, unit: int, status: AirConStatus): - if Service._ready: - Service.update_aircon(target, room, unit, status=status) + def set_aircon_status( + self, target: EnumDevice, room: int, unit: int, status: AirConStatus + ): + if self._ready: + self.update_aircon(target, room, unit, status=status) else: li = [] if target == EnumDevice.AIRCON: - li = Service._aircons + li = self._aircons elif target == EnumDevice.NEWAIRCON: - li = Service._new_aircons + li = self._new_aircons elif target == EnumDevice.BATHROOM: - li = Service._bathrooms + li = self._bathrooms for i in li: if i.unit_id == unit and i.room_id == room: i.status = status - Service._none_stat_dev_cnt -= 1 + self._none_stat_dev_cnt -= 1 break - @staticmethod - def set_sensors_status(sensors: typing.List[Sensor]): + def set_sensors_status(self, sensors: list[Sensor]): for new_sensor in sensors: - for sensor in Service._sensors: + for sensor in self._sensors: if sensor.unique_id == new_sensor.unique_id: for attr in STATUS_ATTR: setattr(sensor, attr, getattr(new_sensor, attr)) break - for item in Service._sensor_hook: + for item in self._sensor_hook: unique_id, func = item if new_sensor.unique_id == unique_id: try: @@ -295,28 +302,29 @@ def set_sensors_status(sensors: typing.List[Sensor]): except Exception as e: _log(str(e)) - @staticmethod - def poll_status(): - for i in Service._new_aircons: + def poll_status(self): + for i in self._new_aircons: p = AirConQueryStatusParam() p.target = EnumDevice.NEWAIRCON p.device = i - Service.send_msg(p) + self.send_msg(p) p = Sensor2InfoParam() - Service.send_msg(p) + self.send_msg(p) - @staticmethod - def update_aircon(target: EnumDevice, room: int, unit: int, **kwargs): - li = Service._status_hook + def update_aircon(self, target: EnumDevice, room: int, unit: int, **kwargs): + li = self._status_hook for item in li: i, func = item - if i.unit_id == unit and i.room_id == room and get_device_by_aircon(i) == target: + if ( + i.unit_id == unit + and i.room_id == room + and get_device_by_aircon(i) == target + ): try: func(**kwargs) except Exception as e: - _log('hook error!!') + _log("hook error!!") _log(str(e)) - @staticmethod - def get_scan_interval(): - return Service._scan_interval + def get_scan_interval(self): + return self._scan_interval diff --git a/custom_components/ds_air/hass_inst.py b/custom_components/ds_air/hass_inst.py deleted file mode 100644 index 01d983f..0000000 --- a/custom_components/ds_air/hass_inst.py +++ /dev/null @@ -1,11 +0,0 @@ - -class GetHass: - HASS = None - - @staticmethod - def set_hass(hass): - GetHass.HASS = hass - - @staticmethod - def get_hash(): - return GetHass.HASS diff --git a/custom_components/ds_air/manifest.json b/custom_components/ds_air/manifest.json index f8ebe21..7c8463a 100644 --- a/custom_components/ds_air/manifest.json +++ b/custom_components/ds_air/manifest.json @@ -1,10 +1,12 @@ { "domain": "ds_air", "name": "DS-AIR", - "documentation": "https://github.com/mypal/ha-dsair", - "dependencies": [], "codeowners": ["@mypal"], + "config_flow": true, + "dependencies": [], + "documentation": "https://github.com/mypal/ha-dsair", + "integration_type": "hub", + "issue_tracker": "https://github.com/mypal/ha-dsair/issues", "requirements": [], - "version": "1.3.5", - "config_flow": true + "version": "2.0.0" } diff --git a/custom_components/ds_air/sensor.py b/custom_components/ds_air/sensor.py index d211b98..947feca 100644 --- a/custom_components/ds_air/sensor.py +++ b/custom_components/ds_air/sensor.py @@ -1,106 +1,63 @@ """Support for Daikin sensors.""" -from typing import Optional -from homeassistant.components.sensor import SensorEntity, SensorStateClass +from homeassistant.components.sensor import SensorEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN, SENSOR_TYPES -from .ds_air_service.dao import Sensor, UNINITIALIZED_VALUE -from .ds_air_service.service import Service +from .const import DOMAIN, MANUFACTURER +from .descriptions import SENSOR_DESCRIPTORS, DsSensorEntityDescription +from .ds_air_service import UNINITIALIZED_VALUE, Sensor, Service -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +): """Perform the setup for Daikin devices.""" + service: Service = hass.data[DOMAIN][config_entry.entry_id] entities = [] - for device in Service.get_sensors(): - for key in SENSOR_TYPES: + for device in service.get_sensors(): + for key in SENSOR_DESCRIPTORS: if config_entry.data.get(key): - entities.append(DsSensor(device, key)) + entities.append(DsSensor(service, device, SENSOR_DESCRIPTORS.get(key))) async_add_entities(entities) class DsSensor(SensorEntity): - """Representation of a DaikinSensor.""" + """Representation of a Daikin Sensor.""" - def __init__(self, device: Sensor, data_key): - """Initialize the DaikinSensor.""" - self._data_key = data_key - self._name = device.alias - self._unique_id = device.unique_id - self._is_available = False - self._state = 0 - self.parse_data(device, True) - Service.register_sensor_hook(device.unique_id, self.parse_data) + entity_description: DsSensorEntityDescription - @property - def name(self): - return "%s_%s" % (self._data_key, self._unique_id) + _attr_should_poll: bool = False - @property - def unique_id(self): - return "%s_%s" % (self._data_key, self._unique_id) + def __init__( + self, service: Service, device: Sensor, description: DsSensorEntityDescription + ): + """Initialize the Daikin Sensor.""" + self.entity_description = description + self._data_key: str = description.key - @property - def device_info(self) -> Optional[DeviceInfo]: - return { - "identifiers": {(DOMAIN, self._unique_id)}, - "name": "传感器%s" % self._name, - "manufacturer": "Daikin Industries, Ltd." - } - - @property - def available(self): - return self._is_available - - @property - def should_poll(self): - return False - - @property - def icon(self): - """Return the icon to use in the frontend.""" - try: - return SENSOR_TYPES.get(self._data_key)[1] - except TypeError: - return None - - @property - def unit_of_measurement(self): - """Return the unit of measurement of this entity, if any.""" - try: - return SENSOR_TYPES.get(self._data_key)[0] - except TypeError: - return None - - @property - def device_class(self): - """Return the device class of this entity.""" - return ( - SENSOR_TYPES.get(self._data_key)[2] - if self._data_key in SENSOR_TYPES - else None + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, device.unique_id)}, + name=device.alias, + manufacturer=MANUFACTURER, ) - - @property - def state_class(self): - """Return the state class of this entity.""" - return SensorStateClass.MEASUREMENT - @property - def state(self): - """Return the state of the sensor.""" - return self._state + self._attr_unique_id = f"{self._data_key}_{device.unique_id}" + self.entity_id = f"sensor.daikin_{device.mac}_{self._data_key}" + + self._parse_data(device) + service.register_sensor_hook(device.unique_id, self._handle_sensor_hook) - def parse_data(self, device: Sensor, not_update: bool = False): + def _parse_data(self, device: Sensor) -> None: """Parse data sent by gateway.""" - self._is_available = device.connected - if UNINITIALIZED_VALUE != getattr(device, self._data_key): - scaling = SENSOR_TYPES.get(self._data_key)[3] - if type(scaling) != int and type(scaling) != float: - self._state = str(getattr(device, self._data_key)) - else: - self._state = getattr(device, self._data_key) / scaling + self._attr_available = device.connected + if (data := getattr(device, self._data_key)) != UNINITIALIZED_VALUE: + self._attr_native_value = self.entity_description.value_fn(data) - if not not_update: - self.schedule_update_ha_state() - return True + def _handle_sensor_hook(self, device: Sensor) -> None: + self._parse_data(device) + self.schedule_update_ha_state() diff --git a/custom_components/ds_air/strings.json b/custom_components/ds_air/strings.json deleted file mode 100644 index 2f4b115..0000000 --- a/custom_components/ds_air/strings.json +++ /dev/null @@ -1,72 +0,0 @@ -{ - "config": { - "step": { - "user": { - "title": "DS-AIR", - "description": "Support DTA117B611/DTA117C611", - "data": { - "host": "Host", - "port": "Port", - "gw": "Gateway model", - "scan_interval": "Sensor update frequency (minutes)", - "sensors": "Has sensor", - "temp": "Create Temperature entity", - "humidity": "Create Humidity entity", - "pm25": "Create PM2.5 entity", - "co2": "Create CO2 entity", - "tvoc": "Create TVOC entity", - "voc": "Create VOC entity", - "hcho": "Create HCHO entity" - } - } - }, - "error": { - }, - "abort": { - "single_instance_allowed": "Only one instance is allowed." - }, - "flow_title": "DS-AIR" - }, - "options": { - "step": { - "init": { - "title": "DS-AIR", - "menu_options": { - "adjust_config": "Adjust config", - "bind_sensors": "Link sensors" - } - }, - "adjust_config": { - "title": "Adjust config", - "description": "", - "data": { - "host": "Gateway IP", - "port": "Gateway Port", - "gw": "Gateway model", - "scan_interval": "Sensor update frequency(min)", - "sensors": "Has sensor", - "temp": "Create Temperature entity", - "humidity": "Create Humidity entity", - "pm25": "Create PM2.5 entity", - "co2": "Create CO2 entity", - "tvoc": "Create TVOC entity", - "voc": "Create VOC entity", - "hcho": "Create HCHO entity" - } - }, - "bind_sensors": { - "title": "Link sensor", - "description": "Link sensor for AC", - "data": { - "climate": "Climate name", - "sensor_temp": "Temperature sensor entity_id", - "sensor_humi": "Humidity sensor entity_id" - }, - "empty": { - "title": "No data", - "description": "No AC for link" - } - } - } - } -} \ No newline at end of file diff --git a/protocol-demo.txt b/tests/protocol-demo.txt similarity index 100% rename from protocol-demo.txt rename to tests/protocol-demo.txt diff --git a/custom_components/ds_air/test.py b/tests/test.py similarity index 59% rename from custom_components/ds_air/test.py rename to tests/test.py index e5f487a..b1555ab 100644 --- a/custom_components/ds_air/test.py +++ b/tests/test.py @@ -1,26 +1,30 @@ -from ds_air_service.display import display -from ds_air_service.decoder import decoder +from custom_components.ds_air import Config +from custom_components.ds_air.ds_air_service.decoder import decoder +from custom_components.ds_air.ds_air_service.display import display list = [ # '0212000d000000010000000000000000001000000103', - # '0211000d0000000500000000000000000001000203', + # '0211000d0000000500000000000000000001000203', ] -def show(s): - if s[0] == 'D': +def show(s, config: Config): + if s[0] == "D": s = s[6:] print(s) b = bytes.fromhex(s) while b: - r, b = decoder(b) + r, b = decoder(b, config) print(display(r)) + +c = Config() for i in list: - show(i) + show(i, c) import socket + def connect(): s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect(("192.168.1.213", 8008)) @@ -28,8 +32,9 @@ def connect(): s.sendall(bytes.fromhex("0213000d00010005000000000000000001300001ffff03")) "0x02 1300 0d00 0100 05000000 000000000001300001ffff03" "0x02 1300 0d00 0100 02000000 000000000001300001ffff03" - while (True): + while True: data = s.recv(1024) - print("0x"+data.hex()) + print("0x" + data.hex()) + -# connect() \ No newline at end of file +# connect()