From 750ff3b71e5647f7669c636709f49aad67eb193d Mon Sep 17 00:00:00 2001 From: Necroneco Date: Mon, 12 Aug 2024 19:36:35 +0800 Subject: [PATCH 01/15] fix a syntax error --- custom_components/ds_air/ds_air_service/ctrl_enum.py | 1 + 1 file changed, 1 insertion(+) 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..d0b5931 100644 --- a/custom_components/ds_air/ds_air_service/ctrl_enum.py +++ b/custom_components/ds_air/ds_air_service/ctrl_enum.py @@ -338,6 +338,7 @@ class EnumControl: def get_mode_name(idx): return _MODE_NAME_LIST[idx] + @staticmethod def get_action_name(idx): return _MODE_ACTION_LIST[idx] From 396106b3302e1554fe0548636593e06920e905a1 Mon Sep 17 00:00:00 2001 From: Necroneco Date: Mon, 4 Sep 2023 23:48:17 +0800 Subject: [PATCH 02/15] Allow the user to delete a device from the UI --- custom_components/ds_air/__init__.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/custom_components/ds_air/__init__.py b/custom_components/ds_air/__init__.py index 34ce21c..fa44216 100644 --- a/custom_components/ds_air/__init__.py +++ b/custom_components/ds_air/__init__.py @@ -7,6 +7,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PORT, CONF_SCAN_INTERVAL 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 @@ -67,3 +68,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def update_listener(hass: HomeAssistant, entry: ConfigEntry): await hass.config_entries.async_reload(entry.entry_id) return True + + +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 From 131ba434c5b871cdc12cbbd22a2bdfe205c494bb Mon Sep 17 00:00:00 2001 From: Necroneco Date: Sun, 29 Oct 2023 14:07:50 +0800 Subject: [PATCH 03/15] Rewrite DsSensor --- custom_components/ds_air/const.py | 81 +++++++++++++++++--- custom_components/ds_air/sensor.py | 117 +++++++++-------------------- 2 files changed, 104 insertions(+), 94 deletions(-) diff --git a/custom_components/ds_air/const.py b/custom_components/ds_air/const.py index 87e9218..1fcd315 100644 --- a/custom_components/ds_air/const.py +++ b/custom_components/ds_air/const.py @@ -1,8 +1,20 @@ -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 collections.abc import Callable +from dataclasses import dataclass +from typing import Any -from .ds_air_service.ctrl_enum import EnumSensor +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, +) DOMAIN = "ds_air" CONF_GW = "gw" @@ -10,12 +22,57 @@ 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], + +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/sensor.py b/custom_components/ds_air/sensor.py index d211b98..ff721c1 100644 --- a/custom_components/ds_air/sensor.py +++ b/custom_components/ds_air/sensor.py @@ -1,106 +1,59 @@ """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 .const import DOMAIN, DsSensorEntityDescription, SENSOR_DESCRIPTORS from .ds_air_service.dao import Sensor, UNINITIALIZED_VALUE from .ds_air_service.service import 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.""" entities = [] for device in Service.get_sensors(): - for key in SENSOR_TYPES: + for key in SENSOR_DESCRIPTORS: if config_entry.data.get(key): - entities.append(DsSensor(device, key)) + entities.append(DsSensor(device, SENSOR_DESCRIPTORS.get(key))) async_add_entities(entities) class DsSensor(SensorEntity): - """Representation of a DaikinSensor.""" - - 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) - - @property - def name(self): - return "%s_%s" % (self._data_key, self._unique_id) - - @property - def unique_id(self): - return "%s_%s" % (self._data_key, self._unique_id) + """Representation of a Daikin Sensor.""" - @property - def device_info(self) -> Optional[DeviceInfo]: - return { - "identifiers": {(DOMAIN, self._unique_id)}, - "name": "传感器%s" % self._name, - "manufacturer": "Daikin Industries, Ltd." - } + entity_description: DsSensorEntityDescription - @property - def available(self): - return self._is_available + _attr_should_poll: bool = False - @property - def should_poll(self): - return False + def __init__(self, device: Sensor, description: DsSensorEntityDescription): + """Initialize the Daikin Sensor.""" + self.entity_description = description + self._data_key: str = description.key - @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="Daikin Industries, Ltd.", ) - - @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() From efeacc82549adf8e6832716d3cea4d25c4cf5b6c Mon Sep 17 00:00:00 2001 From: Necroneco Date: Wed, 22 May 2024 13:02:02 +0800 Subject: [PATCH 04/15] make `ds_air_service` as package --- custom_components/ds_air/__init__.py | 6 ++--- custom_components/ds_air/climate.py | 26 +++++++++---------- custom_components/ds_air/config_flow.py | 2 +- .../ds_air/ds_air_service/__init__.py | 5 ++++ custom_components/ds_air/sensor.py | 4 +-- 5 files changed, 24 insertions(+), 19 deletions(-) create mode 100644 custom_components/ds_air/ds_air_service/__init__.py diff --git a/custom_components/ds_air/__init__.py b/custom_components/ds_air/__init__.py index fa44216..5f56a2b 100644 --- a/custom_components/ds_air/__init__.py +++ b/custom_components/ds_air/__init__.py @@ -11,7 +11,7 @@ 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 .ds_air_service import Config _LOGGER = logging.getLogger(__name__) PLATFORMS = ["climate", "sensor"] @@ -46,7 +46,7 @@ async def async_setup_entry( Config.is_c611 = gw == DEFAULT_GW - from .ds_air_service.service import Service + from .ds_air_service import Service await hass.async_add_executor_job(Service.init, host, port, scan_interval) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(update_listener)) @@ -59,7 +59,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if hass.data[DOMAIN].get("listener") is not None: hass.data[DOMAIN].get("listener")() unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - from .ds_air_service.service import Service + from .ds_air_service import Service Service.destroy() return unload_ok diff --git a/custom_components/ds_air/climate.py b/custom_components/ds_air/climate.py index 2c5e9fb..964be91 100644 --- a/custom_components/ds_air/climate.py +++ b/custom_components/ds_air/climate.py @@ -26,10 +26,10 @@ 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 .ds_air_service import Config +from .ds_air_service import EnumControl +from .ds_air_service import AirCon, AirConStatus +from .ds_air_service import display _SUPPORT_FLAGS = ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE | ClimateEntityFeature.PRESET_MODE # | ClimateEntityFeature.SWING_MODE | ClimateEntityFeature.TARGET_HUMIDITY @@ -56,7 +56,7 @@ async def async_setup_entry( ) -> None: """Set up the climate devices.""" - from .ds_air_service.service import Service + from .ds_air_service import Service climates = [] for aircon in Service.get_aircons(): climates.append(DsAir(aircon)) @@ -107,7 +107,7 @@ def __init__(self, aircon: AirCon): self._link_cur_humi = False self._cur_temp = None self._cur_humi = None - from .ds_air_service.service import Service + from .ds_air_service import Service Service.register_status_hook(aircon, self._status_change_hook) async def async_added_to_hass(self) -> None: @@ -330,7 +330,7 @@ def set_temperature(self, **kwargs): 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 + from .ds_air_service import Service Service.control(self._device_info, new_status) self.schedule_update_ha_state() @@ -342,7 +342,7 @@ def set_humidity(self, humidity): and status.mode in [EnumControl.Mode.RELAX, EnumControl.Mode.SLEEP]: status.humidity = EnumControl.Humidity(humidity) new_status.humidity = EnumControl.Humidity(humidity) - from .ds_air_service.service import Service + from .ds_air_service import Service Service.control(self._device_info, new_status) self.schedule_update_ha_state() @@ -354,7 +354,7 @@ def set_fan_mode(self, fan_mode): 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 + from .ds_air_service import Service Service.control(self._device_info, new_status) self.schedule_update_ha_state() @@ -366,7 +366,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 + from .ds_air_service import Service Service.control(self._device_info, new_status) else: status.switch = EnumControl.Switch.ON @@ -398,7 +398,7 @@ 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 + from .ds_air_service import Service Service.control(self._device_info, new_status) self.schedule_update_ha_state() @@ -411,7 +411,7 @@ def set_swing_mode(self, swing_mode): 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 + from .ds_air_service import Service Service.control(self._device_info, new_status) self.schedule_update_ha_state() @@ -435,7 +435,7 @@ def set_preset_mode(self, preset_mode: str) -> None: mode = m.RELAX status.mode = mode new_status.mode = mode - from .ds_air_service.service import Service + from .ds_air_service import Service Service.control(self._device_info, new_status) self.schedule_update_ha_state() diff --git a/custom_components/ds_air/config_flow.py b/custom_components/ds_air/config_flow.py index d70f9d9..9b920ea 100644 --- a/custom_components/ds_air/config_flow.py +++ b/custom_components/ds_air/config_flow.py @@ -19,7 +19,7 @@ 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 .ds_air_service import Service from .hass_inst import GetHass _LOGGER = logging.getLogger(__name__) 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..391a982 --- /dev/null +++ b/custom_components/ds_air/ds_air_service/__init__.py @@ -0,0 +1,5 @@ +from .config import Config +from .ctrl_enum import EnumControl +from .dao import AirCon, AirConStatus, Sensor, UNINITIALIZED_VALUE +from .display import display +from .service import Service diff --git a/custom_components/ds_air/sensor.py b/custom_components/ds_air/sensor.py index ff721c1..c9b7cb8 100644 --- a/custom_components/ds_air/sensor.py +++ b/custom_components/ds_air/sensor.py @@ -6,8 +6,8 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN, DsSensorEntityDescription, SENSOR_DESCRIPTORS -from .ds_air_service.dao import Sensor, UNINITIALIZED_VALUE -from .ds_air_service.service import Service +from .ds_air_service import Sensor, UNINITIALIZED_VALUE +from .ds_air_service import Service async def async_setup_entry( From 0a65857a6771c1ae14da70697d4f55299fcbe5fd Mon Sep 17 00:00:00 2001 From: Necroneco Date: Wed, 22 May 2024 13:03:53 +0800 Subject: [PATCH 05/15] ruff format --- custom_components/ds_air/__init__.py | 16 +- custom_components/ds_air/climate.py | 102 +++++++---- custom_components/ds_air/config_flow.py | 99 +++++++--- .../ds_air/ds_air_service/base_bean.py | 2 +- .../ds_air/ds_air_service/ctrl_enum.py | 39 +++- .../ds_air/ds_air_service/dao.py | 150 +++++++++------ .../ds_air/ds_air_service/decoder.py | 172 +++++++++++++----- .../ds_air/ds_air_service/display.py | 16 +- .../ds_air/ds_air_service/param.py | 33 ++-- .../ds_air/ds_air_service/service.py | 71 +++++--- custom_components/ds_air/hass_inst.py | 1 - custom_components/ds_air/sensor.py | 4 +- custom_components/ds_air/test.py | 15 +- 13 files changed, 490 insertions(+), 230 deletions(-) diff --git a/custom_components/ds_air/__init__.py b/custom_components/ds_air/__init__.py index 5f56a2b..804c8af 100644 --- a/custom_components/ds_air/__init__.py +++ b/custom_components/ds_air/__init__.py @@ -2,6 +2,7 @@ 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 @@ -9,9 +10,9 @@ 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 .const import CONF_GW, DEFAULT_GW, DEFAULT_HOST, DEFAULT_PORT, DOMAIN from .ds_air_service import Config +from .hass_inst import GetHass _LOGGER = logging.getLogger(__name__) PLATFORMS = ["climate", "sensor"] @@ -22,15 +23,14 @@ def _log(s: str): 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): hass.data.setdefault(DOMAIN, {}) host = entry.data[CONF_HOST] port = entry.data[CONF_PORT] @@ -47,6 +47,7 @@ async def async_setup_entry( Config.is_c611 = gw == DEFAULT_GW from .ds_air_service import Service + await hass.async_add_executor_job(Service.init, host, port, scan_interval) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(update_listener)) @@ -60,6 +61,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN].get("listener")() unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) from .ds_air_service import Service + Service.destroy() return unload_ok @@ -70,6 +72,8 @@ async def update_listener(hass: HomeAssistant, entry: ConfigEntry): return True -async def async_remove_config_entry_device(hass: HomeAssistant, config_entry: ConfigEntry, device_entry: DeviceEntry) -> bool: +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 964be91..cccd494 100644 --- a/custom_components/ds_air/climate.py +++ b/custom_components/ds_air/climate.py @@ -6,57 +6,73 @@ """ import logging -from typing import Optional, List +from typing import List, Optional import voluptuous as vol from homeassistant.components.climate import ( - PLATFORM_SCHEMA, ClimateEntity, ClimateEntityFeature, - HVACMode, HVACAction, - PRESET_NONE, PRESET_SLEEP, PRESET_COMFORT, - FAN_AUTO, FAN_LOW, FAN_MEDIUM, FAN_HIGH + FAN_AUTO, + FAN_HIGH, + FAN_LOW, + FAN_MEDIUM, + HVACAction, + HVACMode, + PLATFORM_SCHEMA, + PRESET_COMFORT, + PRESET_NONE, + PRESET_SLEEP, ) 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, + 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 import Config -from .ds_air_service import EnumControl -from .ds_air_service import AirCon, AirConStatus -from .ds_air_service import display +from .ds_air_service import AirCon, AirConStatus, Config, EnumControl, display -_SUPPORT_FLAGS = ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE | ClimateEntityFeature.PRESET_MODE +_SUPPORT_FLAGS = ( + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.FAN_MODE + | ClimateEntityFeature.PRESET_MODE +) # | ClimateEntityFeature.SWING_MODE | ClimateEntityFeature.TARGET_HUMIDITY 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 = ['➡️', '↘️', '⬇️', '↙️', '⬅️', '↔️', '🔄'] +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 import Service + climates = [] for aircon in Service.get_aircons(): climates.append(DsAir(aircon)) @@ -84,7 +100,9 @@ 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) + 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 @@ -94,7 +112,7 @@ class DsAir(ClimateEntity): _enable_turn_on_off_backwards_compatibility = False # used in 2024.2~2024.12 def __init__(self, aircon: AirCon): - _log('create aircon:') + _log("create aircon:") _log(str(aircon.__dict__)) _log(str(aircon.status.__dict__)) """Initialize the climate device.""" @@ -108,6 +126,7 @@ def __init__(self, aircon: AirCon): self._cur_temp = None self._cur_humi = None from .ds_air_service import Service + Service.register_status_hook(aircon, self._status_change_hook) async def async_added_to_hass(self) -> None: @@ -119,16 +138,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: @@ -311,7 +330,9 @@ def fan_modes(self) -> Optional[List[str]]: @property def swing_mode(self): """Return the swing setting.""" - return EnumControl.get_fan_direction_name(self._device_info.status.fan_direction1.value) + return EnumControl.get_fan_direction_name( + self._device_info.status.fan_direction1.value + ) @property def swing_modes(self) -> Optional[List[str]]: @@ -326,11 +347,14 @@ def set_temperature(self, **kwargs): if 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]: + 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 import Service + Service.control(self._device_info, new_status) self.schedule_update_ha_state() @@ -338,11 +362,14 @@ def set_humidity(self, humidity): """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, + ]: status.humidity = EnumControl.Humidity(humidity) new_status.humidity = EnumControl.Humidity(humidity) from .ds_air_service import Service + Service.control(self._device_info, new_status) self.schedule_update_ha_state() @@ -350,11 +377,14 @@ def set_fan_mode(self, fan_mode): """Set new 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]: + 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 import Service + Service.control(self._device_info, new_status) self.schedule_update_ha_state() @@ -367,6 +397,7 @@ def set_hvac_mode(self, hvac_mode: str) -> None: status.switch = EnumControl.Switch.OFF new_status.switch = EnumControl.Switch.OFF from .ds_air_service import Service + Service.control(self._device_info, new_status) else: status.switch = EnumControl.Switch.ON @@ -399,6 +430,7 @@ def set_hvac_mode(self, hvac_mode: str) -> None: status.mode = mode new_status.mode = mode from .ds_air_service import Service + Service.control(self._device_info, new_status) self.schedule_update_ha_state() @@ -412,6 +444,7 @@ def set_swing_mode(self, swing_mode): 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 import Service + Service.control(self._device_info, new_status) self.schedule_update_ha_state() @@ -436,6 +469,7 @@ def set_preset_mode(self, preset_mode: str) -> None: status.mode = mode new_status.mode = mode from .ds_air_service import Service + Service.control(self._device_info, new_status) self.schedule_update_ha_state() @@ -479,7 +513,7 @@ def device_info(self) -> Optional[DeviceInfo]: return { "identifiers": {(DOMAIN, self.unique_id)}, "name": "空调%s" % self._name, - "manufacturer": "Daikin Industries, Ltd." + "manufacturer": "Daikin Industries, Ltd.", } @property diff --git a/custom_components/ds_air/config_flow.py b/custom_components/ds_air/config_flow.py index 9b920ea..e68e4bd 100644 --- a/custom_components/ds_air/config_flow.py +++ b/custom_components/ds_air/config_flow.py @@ -41,14 +41,19 @@ def __init__(self): self.sensor_check = {} self.user_input = {} - async def async_step_user(self, user_input: dict[str, Any] | None = None) -> FlowResult: + 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") 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 ( + user_input.get(CONF_SENSORS) == False + or user_input.get("temp") is not None + ): return self.async_create_entry(title="金制空气", data=self.user_input) else: return self.async_show_form( @@ -122,20 +127,26 @@ def __init__(self, config_entry: config_entries.ConfigEntry) -> None: self.sensor_check = CONF_SENSORS self.user_input = {} - async def async_step_init(self, user_input: dict[str, Any] | None = None) -> FlowResult: + 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 @@ -144,18 +155,41 @@ async def async_step_adjust_config(self, user_input: dict[str, Any] | None = Non 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_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, + 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, @@ -165,17 +199,28 @@ async def async_step_adjust_config(self, user_input: dict[str, Any] | None = Non 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_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: + 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) @@ -200,12 +245,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/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 d0b5931..4487390 100644 --- a/custom_components/ds_air/ds_air_service/ctrl_enum.py +++ b/custom_components/ds_air/ds_air_service/ctrl_enum.py @@ -241,8 +241,9 @@ class AirFlow(IntEnum): AUTO = 5 -#_AIR_FLOW_NAME_LIST = ['最弱', '稍弱', '中等', '稍强', '最强', '自动'] -_AIR_FLOW_NAME_LIST = [FAN_LOW, '稍弱', FAN_MEDIUM, '稍强', FAN_HIGH, FAN_AUTO] +# _AIR_FLOW_NAME_LIST = ['最弱', '稍弱', '中等', '稍强', '最强', '自动'] +_AIR_FLOW_NAME_LIST = [FAN_LOW, "稍弱", FAN_MEDIUM, "稍强", FAN_HIGH, FAN_AUTO] + class Breathe(IntEnum): CLOSE = 0 @@ -261,7 +262,7 @@ class FanDirection(IntEnum): SWING = 7 -_FAN_DIRECTION_LIST = ['INVALID', '➡️', '↘️', '⬇️', '↙️', '⬅️', '↔️', '🔄'] +_FAN_DIRECTION_LIST = ["INVALID", "➡️", "↘️", "⬇️", "↙️", "⬅️", "↔️", "🔄"] class Humidity(IntEnum): @@ -296,14 +297,36 @@ class Mode(IntEnum): PREHEAT = 8 MOREDRY = 9 + # Legacy Mode Mapping -#_MODE_NAME_LIST = [HVACMode.COOL, HVACMode.DRY, HVACMode.FAN_ONLY, HVACMode.AUTO, HVACMode.HEAT, +# _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] +_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 diff --git a/custom_components/ds_air/ds_air_service/dao.py b/custom_components/ds_air/ds_air_service/dao.py index 38d6350..f0697ce 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 .ctrl_enum import ( + EnumControl, + EnumDevice, + EnumFanDirection, + EnumFanVolume, + EnumOutDoorRunCond, + EnumSwitch, +) class Device: @@ -23,50 +30,54 @@ def _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, + 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: int = current_temp + self.setted_temp: int = setted_temp + self.switch: EnumControl.Switch = switch + self.air_flow: EnumControl.AirFlow = air_flow + self.breathe: EnumControl.Breathe = breathe + self.fan_direction1: EnumControl.FanDirection = fan_direction1 + self.fan_direction2: EnumControl.FanDirection = fan_direction2 + self.humidity: EnumControl.Humidity = humidity + self.mode: EnumControl.Mode = mode class AirCon(Device): def __init__(self): 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.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): @@ -85,7 +96,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 +105,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 +175,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: Optional[Geothermic] = None + self.hd: Optional[HD] = 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: Optional[Ventilation] = 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..16ced81 100644 --- a/custom_components/ds_air/ds_air_service/decoder.py +++ b/custom_components/ds_air/ds_air_service/decoder.py @@ -3,30 +3,74 @@ 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 +from .ctrl_enum import ( + EnumCmdType, + EnumControl, + EnumDevice, + EnumFanDirection, + EnumFanVolume, + EnumOutDoorRunCond, + EnumSensor, + FreshAirHumidification, + ThreeDFresh, +) +from .dao import ( + AirCon, + AirConStatus, + Device, + Geothermic, + HD, + Room, + Sensor, + UNINITIALIZED_VALUE, + Ventilation, + get_device_by_aircon, +) +from .param import ( + AirConCapabilityQueryParam, + AirConQueryStatusParam, + AirConRecommendedIndoorTempParam, + GetRoomInfoParam, + Sensor2InfoParam, +) def decoder(b): if b[0] != 2: return None, None - length = struct.unpack('> 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: @@ -669,8 +735,8 @@ def load_bytes(self, b): 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()) @@ -680,14 +746,26 @@ def load_bytes(self, b): 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) + + 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 @@ -760,16 +838,20 @@ def load_bytes(self, b): def do(self): from .service import Service + 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) @@ -789,10 +871,10 @@ def load_bytes(self, b): 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() + 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..e46a327 100644 --- a/custom_components/ds_air/ds_air_service/display.py +++ b/custom_components/ds_air/ds_air_service/display.py @@ -1,22 +1,22 @@ from enum import Enum -def display(o, d='') -> str: +def display(o, d="") -> str: if type(o) == int or type(o) == str or type(o) == bool or type(o) == float: return str(o) elif isinstance(o, Enum): return o.name elif type(o) == list: - st = '[' + st = "[" for i in range(len(o)): - st += '\n' + d + str(i) + ': ' + display(o[i], d + ' ') - st += ']' + 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__) + ' {' + 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 += '}' + 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..b9b547b 100644 --- a/custom_components/ds_air/ds_air_service/param.py +++ b/custom_components/ds_air/ds_air_service/param.py @@ -2,35 +2,41 @@ 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): @@ -46,7 +52,9 @@ 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 @@ -166,7 +174,7 @@ def __init__(self): class AirConQueryStatusParam(AirconParam): def __init__(self): super().__init__(EnumCmdType.QUERY_STATUS, True) - self._device = None # type: Optional[AirCon] + self._device: Optional[AirCon] = None def generate_subbody(self, s): s.write1(self._device.room_id) @@ -178,7 +186,10 @@ def generate_subbody(self, s): 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 ( + dev.fan_direction1 != EnumFanDirection.FIX + and dev.fan_direction2 != EnumFanDirection.FIX + ): flag = flag | t.FAN_DIRECTION if dev.bath_room: flag = flag | t.BREATHE diff --git a/custom_components/ds_air/ds_air_service/service.py b/custom_components/ds_air/ds_air_service/service.py index fa38198..cd953fe 100644 --- a/custom_components/ds_air/ds_air_service/service.py +++ b/custom_components/ds_air/ds_air_service/service.py @@ -2,20 +2,27 @@ import socket import time import typing -from threading import Thread, Lock +from threading import Lock, Thread 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 AirCon, AirConStatus, Room, STATUS_ATTR, 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) @@ -40,17 +47,17 @@ def do_connect(self): self._s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try: self._s.connect((self._host, self._port)) - _log('connected') + _log("connected") return True except socket.error as exc: - _log('connected error') + _log("connected error") _log(str(exc)) return False 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().hex()) + _log("\033[31msend:\033[0m") _log(display(p)) done = False while not done: @@ -77,7 +84,7 @@ 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) @@ -103,7 +110,7 @@ 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: @@ -138,18 +145,18 @@ def run(self) -> None: 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)] + _socket_client: SocketClient = None + _rooms: typing.List[Room] = None + _aircons: typing.List[AirCon] = None + _new_aircons: typing.List[AirCon] = None + _bathrooms: typing.List[AirCon] = None + _ready: bool = False + _none_stat_dev_cnt: int = 0 + _status_hook: typing.List[(AirCon, typing.Callable)] = [] + _sensor_hook: typing.List[(str, typing.Callable)] = [] _heartbeat_thread = None - _sensors = [] # type: typing.List[Sensor] - _scan_interval = 5 # type: int + _sensors: typing.List[Sensor] = [] + _scan_interval: int = 5 @staticmethod def init(host: str, port: int, scan_interval: int): @@ -160,8 +167,12 @@ def init(host: str, port: int, scan_interval: int): 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: + while ( + Service._rooms is None + or Service._aircons is None + or Service._new_aircons is None + or Service._bathrooms is None + ): time.sleep(1) for i in Service._aircons: for j in Service._rooms: @@ -262,7 +273,9 @@ def set_device(t: EnumDevice, v: typing.List[AirCon]): Service._bathrooms = v @staticmethod - def set_aircon_status(target: EnumDevice, room: int, unit: int, status: AirConStatus): + def set_aircon_status( + target: EnumDevice, room: int, unit: int, status: AirConStatus + ): if Service._ready: Service.update_aircon(target, room, unit, status=status) else: @@ -310,11 +323,15 @@ def update_aircon(target: EnumDevice, room: int, unit: int, **kwargs): li = Service._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 diff --git a/custom_components/ds_air/hass_inst.py b/custom_components/ds_air/hass_inst.py index 01d983f..bb3d697 100644 --- a/custom_components/ds_air/hass_inst.py +++ b/custom_components/ds_air/hass_inst.py @@ -1,4 +1,3 @@ - class GetHass: HASS = None diff --git a/custom_components/ds_air/sensor.py b/custom_components/ds_air/sensor.py index c9b7cb8..5aee200 100644 --- a/custom_components/ds_air/sensor.py +++ b/custom_components/ds_air/sensor.py @@ -1,4 +1,5 @@ """Support for Daikin sensors.""" + from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -6,8 +7,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN, DsSensorEntityDescription, SENSOR_DESCRIPTORS -from .ds_air_service import Sensor, UNINITIALIZED_VALUE -from .ds_air_service import Service +from .ds_air_service import Sensor, Service, UNINITIALIZED_VALUE async def async_setup_entry( diff --git a/custom_components/ds_air/test.py b/custom_components/ds_air/test.py index e5f487a..bfe2567 100644 --- a/custom_components/ds_air/test.py +++ b/custom_components/ds_air/test.py @@ -1,14 +1,14 @@ -from ds_air_service.display import display from ds_air_service.decoder import decoder +from ds_air_service.display import display list = [ # '0212000d000000010000000000000000001000000103', - # '0211000d0000000500000000000000000001000203', + # '0211000d0000000500000000000000000001000203', ] def show(s): - if s[0] == 'D': + if s[0] == "D": s = s[6:] print(s) b = bytes.fromhex(s) @@ -16,11 +16,13 @@ def show(s): r, b = decoder(b) print(display(r)) + for i in list: show(i) import socket + def connect(): s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect(("192.168.1.213", 8008)) @@ -28,8 +30,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() From 84b582c62b565de4acd83df74384bea6887906d4 Mon Sep 17 00:00:00 2001 From: Necroneco Date: Wed, 22 May 2024 17:34:47 +0800 Subject: [PATCH 06/15] update doc --- .github/workflows/validation.yaml | 16 +++++ README.md | 21 ++++-- custom_components/ds_air/manifest.json | 10 +-- custom_components/ds_air/strings.json | 72 -------------------- protocol-demo.txt => tests/protocol-demo.txt | 0 {custom_components/ds_air => tests}/test.py | 4 +- 6 files changed, 41 insertions(+), 82 deletions(-) create mode 100644 .github/workflows/validation.yaml delete mode 100644 custom_components/ds_air/strings.json rename protocol-demo.txt => tests/protocol-demo.txt (100%) rename {custom_components/ds_air => tests}/test.py (85%) 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/manifest.json b/custom_components/ds_air/manifest.json index f8ebe21..c12bab2 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": "1.3.5" } 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 85% rename from custom_components/ds_air/test.py rename to tests/test.py index bfe2567..fd1f252 100644 --- a/custom_components/ds_air/test.py +++ b/tests/test.py @@ -1,5 +1,5 @@ -from ds_air_service.decoder import decoder -from ds_air_service.display import display +from custom_components.ds_air.ds_air_service.decoder import decoder +from custom_components.ds_air.ds_air_service.display import display list = [ # '0212000d000000010000000000000000001000000103', From 21e629ba5f93e3eade4af02384ac8bd937500e2e Mon Sep 17 00:00:00 2001 From: Necroneco Date: Wed, 22 May 2024 20:37:29 +0800 Subject: [PATCH 07/15] refactor --- custom_components/ds_air/__init__.py | 30 +-- custom_components/ds_air/climate.py | 190 +++++------------- custom_components/ds_air/config_flow.py | 42 ++-- custom_components/ds_air/const.py | 2 + .../ds_air/ds_air_service/service.py | 28 +-- custom_components/ds_air/hass_inst.py | 10 - custom_components/ds_air/sensor.py | 4 +- 7 files changed, 98 insertions(+), 208 deletions(-) delete mode 100644 custom_components/ds_air/hass_inst.py diff --git a/custom_components/ds_air/__init__.py b/custom_components/ds_air/__init__.py index 804c8af..e64f338 100644 --- a/custom_components/ds_air/__init__.py +++ b/custom_components/ds_air/__init__.py @@ -6,28 +6,19 @@ 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 .const import CONF_GW, DEFAULT_GW, DEFAULT_HOST, DEFAULT_PORT, DOMAIN -from .ds_air_service import Config -from .hass_inst import GetHass +from .ds_air_service import Config, Service _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["climate", "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 +PLATFORMS = [ + Platform.CLIMATE, + Platform.SENSOR, +] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): @@ -37,7 +28,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): gw = entry.data[CONF_GW] scan_interval = entry.data[CONF_SCAN_INTERVAL] - _log(f"{host}:{port} {gw} {scan_interval}") + _LOGGER.debug(f"{host}:{port} {gw} {scan_interval}") hass.data[DOMAIN][CONF_HOST] = host hass.data[DOMAIN][CONF_PORT] = port @@ -46,8 +37,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): Config.is_c611 = gw == DEFAULT_GW - from .ds_air_service import Service - await hass.async_add_executor_job(Service.init, host, port, scan_interval) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(update_listener)) @@ -60,16 +49,13 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if hass.data[DOMAIN].get("listener") is not None: hass.data[DOMAIN].get("listener")() unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - from .ds_air_service import Service - Service.destroy() return unload_ok -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) - return True async def async_remove_config_entry_device( diff --git a/custom_components/ds_air/climate.py b/custom_components/ds_air/climate.py index cccd494..c6074a0 100644 --- a/custom_components/ds_air/climate.py +++ b/custom_components/ds_air/climate.py @@ -38,15 +38,14 @@ 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 import AirCon, AirConStatus, Config, EnumControl, display +from .const import DOMAIN, MANUFACTURER +from .ds_air_service import AirCon, AirConStatus, Config, EnumControl, display, Service _SUPPORT_FLAGS = ( ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE | ClimateEntityFeature.PRESET_MODE ) -# | ClimateEntityFeature.SWING_MODE | ClimateEntityFeature.TARGET_HUMIDITY if (MAJOR_VERSION, MINOR_VERSION) >= (2024, 2): _SUPPORT_FLAGS |= ClimateEntityFeature.TURN_ON | ClimateEntityFeature.TURN_OFF @@ -71,8 +70,6 @@ async def async_setup_entry( ) -> None: """Set up the climate devices.""" - from .ds_air_service import Service - climates = [] for aircon in Service.get_aircons(): climates.append(DsAir(aircon)) @@ -109,26 +106,44 @@ async def listener(event: Event): class DsAir(ClimateEntity): """Representation of a Daikin climate device.""" - _enable_turn_on_off_backwards_compatibility = False # used in 2024.2~2024.12 + _attr_should_poll: bool = False + + _attr_fan_modes: list[str] | None = FAN_LIST + # _attr_max_humidity: float = 3 + _attr_max_temp: float = 32 + # _attr_min_humidity: float = 1 + _attr_min_temp: float = 16 + _attr_swing_modes: list[str] | None = SWING_LIST + _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, aircon: AirCon): _log("create aircon:") _log(str(aircon.__dict__)) _log(str(aircon.status.__dict__)) """Initialize the climate device.""" - self._name = aircon.alias + self._attr_name = aircon.alias 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 import Service Service.register_status_hook(aircon, self._status_change_hook) + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, self.unique_id)}, + name=f"空调{self._attr_name}", + manufacturer=MANUFACTURER, + ) + async def async_added_to_hass(self) -> None: if self.linked_temp_entity_id: if state := self.hass.states.get(self.linked_temp_entity_id): @@ -186,27 +201,12 @@ def update_cur_humi(self, value): 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): + def hvac_action(self) -> HVACAction | None: """Return current operation ie. heat, cool, idle.""" if self._device_info.status.switch == EnumControl.Switch.OFF: return HVACAction.OFF @@ -214,7 +214,7 @@ def hvac_action(self): return EnumControl.get_action_name(self._device_info.status.mode.value) @property - def hvac_mode(self) -> str: + def hvac_mode(self) -> HVACMode | None: """Return hvac operation ie. heat, cool mode. Need to be one of HVAC_MODE_*. @@ -225,7 +225,7 @@ def hvac_mode(self) -> str: return EnumControl.get_mode_name(self._device_info.status.mode.value) @property - def hvac_modes(self): + def hvac_modes(self) -> list[HVACMode]: """Return the list of supported features.""" li = [] aircon = self._device_info @@ -243,7 +243,7 @@ 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 @@ -254,27 +254,12 @@ def current_temperature(self): 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): + def current_humidity(self) -> float | None: """Return the current humidity.""" if self._link_cur_humi: return self._cur_humi @@ -282,10 +267,10 @@ def current_humidity(self): 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 @@ -295,10 +280,10 @@ def preset_mode(self) -> Optional[str]: 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 @@ -310,55 +295,38 @@ 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.""" + Requires ClimateEntityFeature.FAN_MODE. + """ 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. + def swing_mode(self) -> str | None: + """Return the swing setting. - Requires SUPPORT_FAN_MODE. + Requires ClimateEntityFeature.SWING_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 ) - @property - def swing_modes(self) -> Optional[List[str]]: - """Return the list of available swing modes. - - Requires SUPPORT_SWING_MODE. - """ - return SWING_LIST - - 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 import Service - + status.setted_temp = round(temperate * 10.0) + new_status.setted_temp = round(temperate * 10.0) Service.control(self._device_info, new_status) self.schedule_update_ha_state() - def set_humidity(self, humidity): + def set_humidity(self, humidity) -> None: """Set new humidity level.""" status = self._device_info.status new_status = AirConStatus() @@ -368,12 +336,10 @@ def set_humidity(self, humidity): ]: status.humidity = EnumControl.Humidity(humidity) new_status.humidity = EnumControl.Humidity(humidity) - from .ds_air_service import Service - Service.control(self._device_info, new_status) self.schedule_update_ha_state() - def set_fan_mode(self, fan_mode): + def set_fan_mode(self, fan_mode: str) -> None: """Set new fan mode.""" status = self._device_info.status new_status = AirConStatus() @@ -383,8 +349,6 @@ def set_fan_mode(self, fan_mode): ]: 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 import Service - Service.control(self._device_info, new_status) self.schedule_update_ha_state() @@ -396,8 +360,6 @@ 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 import Service - Service.control(self._device_info, new_status) else: status.switch = EnumControl.Switch.ON @@ -429,12 +391,10 @@ def set_hvac_mode(self, hvac_mode: str) -> None: mode = m.SLEEP status.mode = mode new_status.mode = mode - from .ds_air_service import Service - 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() @@ -443,8 +403,6 @@ def set_swing_mode(self, swing_mode): 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 import Service - Service.control(self._device_info, new_status) self.schedule_update_ha_state() @@ -468,54 +426,16 @@ def set_preset_mode(self, preset_mode: str) -> None: mode = m.RELAX status.mode = mode new_status.mode = mode - from .ds_air_service import Service - 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 = 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 = 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 e68e4bd..657a830 100644 --- a/custom_components/ds_air/config_flow.py +++ b/custom_components/ds_air/config_flow.py @@ -4,9 +4,8 @@ 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 +14,11 @@ 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 import Service -from .hass_inst import GetHass _LOGGER = logging.getLogger(__name__) @@ -31,14 +29,10 @@ 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( @@ -51,7 +45,7 @@ async def async_step_user( if user_input is not None: self.user_input.update(user_input) if ( - user_input.get(CONF_SENSORS) == False + not user_input.get(CONF_SENSORS) or user_input.get("temp") is not None ): return self.async_create_entry(title="金制空气", data=self.user_input) @@ -93,16 +87,25 @@ def async_get_options_flow(config_entry: ConfigEntry) -> DsAirOptionsFlowHandler 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._sensors_temp = {} + self._sensors_humi = {} + self._len = len(self._climates) + self._cur = -1 + self.user_input = {} + + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Manage the options.""" + sensors = self.hass.states.async_all("sensor") self._sensors_temp = { None: 'None', **{ @@ -119,18 +122,7 @@ def __init__(self, config_entry: config_entries.ConfigEntry) -> None: 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"], diff --git a/custom_components/ds_air/const.py b/custom_components/ds_air/const.py index 1fcd315..4499611 100644 --- a/custom_components/ds_air/const.py +++ b/custom_components/ds_air/const.py @@ -23,6 +23,8 @@ DEFAULT_GW = "DTA117C611" GW_LIST = ["DTA117C611", "DTA117B611"] +MANUFACTURER = "Daikin Industries, Ltd." + FROZEN = MAJOR_VERSION >= 2024 diff --git a/custom_components/ds_air/ds_air_service/service.py b/custom_components/ds_air/ds_air_service/service.py index cd953fe..70017bd 100644 --- a/custom_components/ds_air/ds_air_service/service.py +++ b/custom_components/ds_air/ds_air_service/service.py @@ -1,7 +1,7 @@ import logging import socket import time -import typing +from collections.abc import Callable from threading import Lock, Thread from .ctrl_enum import EnumDevice @@ -69,7 +69,7 @@ def send(self, p: Param): self.do_connect() self._locker.release() - def recv(self) -> (typing.List[BaseResult], bytes): + def recv(self) -> (list[BaseResult], bytes): res = [] done = False data = None @@ -146,16 +146,16 @@ def run(self) -> None: class Service: _socket_client: SocketClient = None - _rooms: typing.List[Room] = None - _aircons: typing.List[AirCon] = None - _new_aircons: typing.List[AirCon] = None - _bathrooms: typing.List[AirCon] = None + _rooms: list[Room] = None + _aircons: list[AirCon] = None + _new_aircons: list[AirCon] = None + _bathrooms: list[AirCon] = None _ready: bool = False _none_stat_dev_cnt: int = 0 - _status_hook: typing.List[(AirCon, typing.Callable)] = [] - _sensor_hook: typing.List[(str, typing.Callable)] = [] + _status_hook: list[(AirCon, Callable)] = [] + _sensor_hook: list[(str, Callable)] = [] _heartbeat_thread = None - _sensors: typing.List[Sensor] = [] + _sensors: list[Sensor] = [] _scan_interval: int = 5 @staticmethod @@ -228,11 +228,11 @@ def control(aircon: AirCon, status: AirConStatus): Service.send_msg(p) @staticmethod - def register_status_hook(device: AirCon, hook: typing.Callable): + def register_status_hook(device: AirCon, hook: Callable): Service._status_hook.append((device, hook)) @staticmethod - def register_sensor_hook(unique_id: str, hook: typing.Callable): + def register_sensor_hook(unique_id: str, hook: Callable): Service._sensor_hook.append((unique_id, hook)) # ----split line---- above for component, below for inner call @@ -251,7 +251,7 @@ def get_rooms(): return Service._rooms @staticmethod - def set_rooms(v: typing.List[Room]): + def set_rooms(v: list[Room]): Service._rooms = v @staticmethod @@ -263,7 +263,7 @@ def set_sensors(sensors): Service._sensors = sensors @staticmethod - def set_device(t: EnumDevice, v: typing.List[AirCon]): + def set_device(t: EnumDevice, v: list[AirCon]): Service._none_stat_dev_cnt += len(v) if t == EnumDevice.AIRCON: Service._aircons = v @@ -293,7 +293,7 @@ def set_aircon_status( break @staticmethod - def set_sensors_status(sensors: typing.List[Sensor]): + def set_sensors_status(sensors: list[Sensor]): for new_sensor in sensors: for sensor in Service._sensors: if sensor.unique_id == new_sensor.unique_id: diff --git a/custom_components/ds_air/hass_inst.py b/custom_components/ds_air/hass_inst.py deleted file mode 100644 index bb3d697..0000000 --- a/custom_components/ds_air/hass_inst.py +++ /dev/null @@ -1,10 +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/sensor.py b/custom_components/ds_air/sensor.py index 5aee200..96b4371 100644 --- a/custom_components/ds_air/sensor.py +++ b/custom_components/ds_air/sensor.py @@ -6,7 +6,7 @@ from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN, DsSensorEntityDescription, SENSOR_DESCRIPTORS +from .const import DOMAIN, DsSensorEntityDescription, MANUFACTURER, SENSOR_DESCRIPTORS from .ds_air_service import Sensor, Service, UNINITIALIZED_VALUE @@ -39,7 +39,7 @@ def __init__(self, device: Sensor, description: DsSensorEntityDescription): self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, device.unique_id)}, name=device.alias, - manufacturer="Daikin Industries, Ltd.", + manufacturer=MANUFACTURER, ) self._attr_unique_id = f"{self._data_key}_{device.unique_id}" From 7a6ab4e1eda2ac1e72df11a2fe3ed9e5380cc613 Mon Sep 17 00:00:00 2001 From: Necroneco Date: Tue, 20 Aug 2024 12:49:12 +0800 Subject: [PATCH 08/15] move ha const out of lib --- custom_components/ds_air/climate.py | 117 +++++----- custom_components/ds_air/config_flow.py | 20 +- custom_components/ds_air/const.py | 136 ++++++----- custom_components/ds_air/descriptions.py | 70 ++++++ .../ds_air/ds_air_service/ctrl_enum.py | 212 +++++------------- .../ds_air/ds_air_service/dao.py | 5 +- custom_components/ds_air/sensor.py | 3 +- 7 files changed, 260 insertions(+), 303 deletions(-) create mode 100644 custom_components/ds_air/descriptions.py diff --git a/custom_components/ds_air/climate.py b/custom_components/ds_air/climate.py index c6074a0..d9e1c5d 100644 --- a/custom_components/ds_air/climate.py +++ b/custom_components/ds_air/climate.py @@ -6,16 +6,11 @@ """ import logging -from typing import List, Optional import voluptuous as vol from homeassistant.components.climate import ( ClimateEntity, ClimateEntityFeature, - FAN_AUTO, - FAN_HIGH, - FAN_LOW, - FAN_MEDIUM, HVACAction, HVACMode, PLATFORM_SCHEMA, @@ -30,7 +25,7 @@ CONF_PORT, MAJOR_VERSION, MINOR_VERSION, - UnitOfTemperature, + PRECISION_TENTHS, UnitOfTemperature, ) from homeassistant.core import Event, HomeAssistant from homeassistant.helpers import config_validation as cv @@ -38,9 +33,20 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_state_change_event -from .const import DOMAIN, MANUFACTURER +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, Config, EnumControl, display, Service + _SUPPORT_FLAGS = ( ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE @@ -49,9 +55,6 @@ 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} ) @@ -106,14 +109,17 @@ async def listener(event: Event): class DsAir(ClimateEntity): """Representation of a Daikin climate device.""" + # Entity Properties _attr_should_poll: bool = False - _attr_fan_modes: list[str] | None = FAN_LIST + # 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_swing_modes: list[str] | None = SWING_LIST + _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 @@ -132,9 +138,6 @@ def __init__(self, aircon: AirCon): 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 Service.register_status_hook(aircon, self._status_change_hook) @@ -184,20 +187,19 @@ 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 @@ -207,26 +209,21 @@ def target_humidity(self) -> float | None: @property def hvac_action(self) -> HVACAction | None: - """Return current operation ie. heat, cool, idle.""" + """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) -> HVACMode | None: - """Return hvac operation ie. heat, cool mode. - - Need to be one of HVAC_MODE_*. - """ + """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) -> list[HVACMode]: - """Return the list of supported features.""" + """Return the list of available hvac operation modes.""" li = [] aircon = self._device_info if aircon.cool_mode: @@ -246,7 +243,7 @@ def hvac_modes(self) -> list[HVACMode]: def current_temperature(self) -> float | None: """Return the current temperature.""" if self._link_cur_temp: - return self._cur_temp + return self._attr_current_temperature else: if Config.is_c611: return None @@ -258,14 +255,6 @@ def target_temperature(self) -> float | None: """Return the temperature we try to reach.""" return self._device_info.status.setted_temp / 10 - @property - def current_humidity(self) -> float | None: - """Return the current humidity.""" - if self._link_cur_humi: - return self._cur_humi - else: - return None - @property def preset_mode(self) -> str | None: """Return the current preset mode, e.g., home, away, temp. @@ -274,10 +263,9 @@ def preset_mode(self) -> str | None: """ 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) -> list[str] | None: @@ -300,7 +288,7 @@ def fan_mode(self) -> str | None: Requires ClimateEntityFeature.FAN_MODE. """ - return EnumControl.get_air_flow_name(self._device_info.status.air_flow.value) + return get_air_flow_name(self._device_info.status.air_flow.value) @property def swing_mode(self) -> str | None: @@ -308,51 +296,49 @@ def swing_mode(self) -> str | None: Requires ClimateEntityFeature.SWING_MODE. """ - return EnumControl.get_fan_direction_name( - self._device_info.status.fan_direction1.value - ) + return get_fan_direction_name(self._device_info.status.fan_direction1.value) def set_temperature(self, **kwargs) -> None: """Set new target temperatures.""" 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, ]: + new_status = AirConStatus() status.setted_temp = round(temperate * 10.0) new_status.setted_temp = round(temperate * 10.0) Service.control(self._device_info, new_status) - self.schedule_update_ha_state() + self.schedule_update_ha_state() - def set_humidity(self, humidity) -> None: + 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, ]: + new_status = AirConStatus() status.humidity = EnumControl.Humidity(humidity) new_status.humidity = EnumControl.Humidity(humidity) Service.control(self._device_info, new_status) - self.schedule_update_ha_state() + self.schedule_update_ha_state() def set_fan_mode(self, fan_mode: str) -> None: - """Set new fan mode.""" + """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) + new_status = AirConStatus() + status.air_flow = get_air_flow_enum(fan_mode) + new_status.air_flow = get_air_flow_enum(fan_mode) Service.control(self._device_info, new_status) - self.schedule_update_ha_state() + 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 @@ -397,16 +383,17 @@ def set_hvac_mode(self, hvac_mode: str) -> None: 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) + status.fan_direction2 = get_fan_direction_enum(swing_mode) + new_status.fan_direction2 = get_fan_direction_enum(swing_mode) Service.control(self._device_info, new_status) - self.schedule_update_ha_state() + 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() @@ -435,7 +422,7 @@ def supported_features(self) -> ClimateEntityFeature: flags = _SUPPORT_FLAGS aircon = self._device_info if aircon.status.fan_direction1.value > 0: - flags = flags | ClimateEntityFeature.SWING_MODE + flags |= ClimateEntityFeature.SWING_MODE if aircon.relax_mode: - flags = flags | ClimateEntityFeature.TARGET_HUMIDITY + 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 657a830..106da45 100644 --- a/custom_components/ds_air/config_flow.py +++ b/custom_components/ds_air/config_flow.py @@ -44,10 +44,7 @@ async def async_step_user( errors = {} if user_input is not None: self.user_input.update(user_input) - if ( - not user_input.get(CONF_SENSORS) - 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( @@ -107,20 +104,21 @@ async def async_step_init( """Manage the options.""" 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 - } + }, } return self.async_show_menu( @@ -229,7 +227,9 @@ async def async_step_bind_sensors( 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( diff --git a/custom_components/ds_air/const.py b/custom_components/ds_air/const.py index 4499611..ef6519f 100644 --- a/custom_components/ds_air/const.py +++ b/custom_components/ds_air/const.py @@ -1,21 +1,15 @@ -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, +from homeassistant.components.climate import ( + FAN_AUTO, + FAN_HIGH, + FAN_LOW, + FAN_MEDIUM, + HVACAction, + HVACMode, ) +from .ds_air_service import EnumControl + + DOMAIN = "ds_air" CONF_GW = "gw" DEFAULT_HOST = "192.168.1." @@ -25,56 +19,60 @@ MANUFACTURER = "Daikin Industries, Ltd." -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, - ), -} + +_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..8e78678 --- /dev/null +++ b/custom_components/ds_air/descriptions.py @@ -0,0 +1,70 @@ +from dataclasses import dataclass +from typing import Any, Callable + +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/ctrl_enum.py b/custom_components/ds_air/ds_air_service/ctrl_enum.py index 4487390..9dc6a08 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,49 +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 @@ -285,112 +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] - - @staticmethod - 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 diff --git a/custom_components/ds_air/ds_air_service/dao.py b/custom_components/ds_air/ds_air_service/dao.py index f0697ce..c1df073 100644 --- a/custom_components/ds_air/ds_air_service/dao.py +++ b/custom_components/ds_air/ds_air_service/dao.py @@ -22,13 +22,10 @@ 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, diff --git a/custom_components/ds_air/sensor.py b/custom_components/ds_air/sensor.py index 96b4371..325773e 100644 --- a/custom_components/ds_air/sensor.py +++ b/custom_components/ds_air/sensor.py @@ -6,7 +6,8 @@ from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN, DsSensorEntityDescription, MANUFACTURER, SENSOR_DESCRIPTORS +from .const import DOMAIN, MANUFACTURER +from .descriptions import DsSensorEntityDescription, SENSOR_DESCRIPTORS from .ds_air_service import Sensor, Service, UNINITIALIZED_VALUE From f59c0114c8555006c564e97ea6be41fb51b8d5a2 Mon Sep 17 00:00:00 2001 From: Necroneco Date: Tue, 20 Aug 2024 13:11:29 +0800 Subject: [PATCH 09/15] has_entity_name --- custom_components/ds_air/climate.py | 13 +- custom_components/ds_air/config_flow.py | 111 +++++++----------- .../ds_air/ds_air_service/service.py | 2 +- 3 files changed, 53 insertions(+), 73 deletions(-) diff --git a/custom_components/ds_air/climate.py b/custom_components/ds_air/climate.py index d9e1c5d..a4f1d99 100644 --- a/custom_components/ds_air/climate.py +++ b/custom_components/ds_air/climate.py @@ -25,7 +25,8 @@ CONF_PORT, MAJOR_VERSION, MINOR_VERSION, - PRECISION_TENTHS, UnitOfTemperature, + PRECISION_TENTHS, + UnitOfTemperature, ) from homeassistant.core import Event, HomeAssistant from homeassistant.helpers import config_validation as cv @@ -39,7 +40,8 @@ FAN_DIRECTION_LIST, MANUFACTURER, get_action_name, - get_air_flow_enum, get_air_flow_name, + get_air_flow_enum, + get_air_flow_name, get_fan_direction_enum, get_fan_direction_name, get_mode_name, @@ -83,7 +85,7 @@ 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 @@ -110,6 +112,8 @@ class DsAir(ClimateEntity): """Representation of a Daikin climate device.""" # Entity Properties + _attr_has_entity_name: bool = True + _attr_name: str | None = None _attr_should_poll: bool = False # Climate Properties @@ -132,7 +136,6 @@ def __init__(self, aircon: AirCon): _log(str(aircon.__dict__)) _log(str(aircon.status.__dict__)) """Initialize the climate device.""" - self._attr_name = aircon.alias self._device_info = aircon self._attr_unique_id = aircon.unique_id self.linked_temp_entity_id: str | None = None @@ -143,7 +146,7 @@ def __init__(self, aircon: AirCon): self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, self.unique_id)}, - name=f"空调{self._attr_name}", + name=aircon.alias if "空调" in aircon.alias else f"{aircon.alias} 空调", manufacturer=MANUFACTURER, ) diff --git a/custom_components/ds_air/config_flow.py b/custom_components/ds_air/config_flow.py index 106da45..14495a8 100644 --- a/custom_components/ds_air/config_flow.py +++ b/custom_components/ds_air/config_flow.py @@ -140,73 +140,50 @@ async def async_step_adjust_config( 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, - ) + 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 diff --git a/custom_components/ds_air/ds_air_service/service.py b/custom_components/ds_air/ds_air_service/service.py index 70017bd..9f0392c 100644 --- a/custom_components/ds_air/ds_air_service/service.py +++ b/custom_components/ds_air/ds_air_service/service.py @@ -212,7 +212,7 @@ def destroy(): Service._ready = False @staticmethod - def get_aircons(): + def get_aircons() -> list[AirCon]: aircons = [] if Service._new_aircons is not None: aircons += Service._new_aircons From 4b44e9811a71373f9318f9b1237f7f0c7da8db37 Mon Sep 17 00:00:00 2001 From: Necroneco Date: Sat, 31 Aug 2024 12:31:39 +0800 Subject: [PATCH 10/15] make `Config` no longer a global variable --- custom_components/ds_air/__init__.py | 7 +- custom_components/ds_air/climate.py | 6 +- custom_components/ds_air/config_flow.py | 4 +- .../ds_air/ds_air_service/dao.py | 4 +- .../ds_air/ds_air_service/decoder.py | 85 ++++++++++--------- .../ds_air/ds_air_service/param.py | 24 +++--- .../ds_air/ds_air_service/service.py | 14 +-- tests/test.py | 8 +- 8 files changed, 80 insertions(+), 72 deletions(-) diff --git a/custom_components/ds_air/__init__.py b/custom_components/ds_air/__init__.py index e64f338..369e473 100644 --- a/custom_components/ds_air/__init__.py +++ b/custom_components/ds_air/__init__.py @@ -21,7 +21,7 @@ ] -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] @@ -35,9 +35,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): hass.data[DOMAIN][CONF_GW] = gw hass.data[DOMAIN][CONF_SCAN_INTERVAL] = scan_interval - Config.is_c611 = gw == DEFAULT_GW + config = Config() + config.is_c611 = gw == DEFAULT_GW - await hass.async_add_executor_job(Service.init, host, port, scan_interval) + 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)) diff --git a/custom_components/ds_air/climate.py b/custom_components/ds_air/climate.py index a4f1d99..540a376 100644 --- a/custom_components/ds_air/climate.py +++ b/custom_components/ds_air/climate.py @@ -85,7 +85,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._device_info.alias == 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 @@ -248,7 +250,7 @@ def current_temperature(self) -> float | None: if self._link_cur_temp: return self._attr_current_temperature else: - if Config.is_c611: + if self._device_info.config.is_c611: return None else: return self._device_info.status.current_temp / 10 diff --git a/custom_components/ds_air/config_flow.py b/custom_components/ds_air/config_flow.py index 14495a8..4650b26 100644 --- a/custom_components/ds_air/config_flow.py +++ b/custom_components/ds_air/config_flow.py @@ -148,9 +148,7 @@ async def async_step_adjust_config( { 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_GW, default=data[CONF_GW]): vol.In(GW_LIST), vol.Required( CONF_SCAN_INTERVAL, default=data[CONF_SCAN_INTERVAL] ): int, diff --git a/custom_components/ds_air/ds_air_service/dao.py b/custom_components/ds_air/ds_air_service/dao.py index c1df073..26ef804 100644 --- a/custom_components/ds_air/ds_air_service/dao.py +++ b/custom_components/ds_air/ds_air_service/dao.py @@ -9,6 +9,7 @@ EnumOutDoorRunCond, EnumSwitch, ) +from .config import Config class Device: @@ -51,8 +52,9 @@ def __init__( class AirCon(Device): - def __init__(self): + def __init__(self, config: Config): super().__init__() + self.config = config self.auto_dry_mode: int = 0 self.auto_mode: int = 0 self.bath_room: bool = False diff --git a/custom_components/ds_air/ds_air_service/decoder.py b/custom_components/ds_air/ds_air_service/decoder.py index 16ced81..7e0f33c 100644 --- a/custom_components/ds_air/ds_air_service/decoder.py +++ b/custom_components/ds_air/ds_air_service/decoder.py @@ -35,7 +35,7 @@ ) -def decoder(b): +def decoder(b: bytes, config: Config): if b[0] != 2: return None, None @@ -51,11 +51,12 @@ def decoder(b): return None, None return result_factory( - struct.unpack(" None: """do nothing""" - def do(self): + def do(self) -> None: """do nothing""" @@ -199,8 +200,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() @@ -301,7 +302,7 @@ def load_bytes(self, b): self._sensors.append(sensor) count = count - 1 - def do(self): + def do(self) -> None: from .service import Service Service.set_sensors_status(self._sensors) @@ -333,7 +334,7 @@ def __init__(self, cmd_id: int, target: EnumDevice): self._cmdId = None self._code = None - def load_bytes(self, b): + def load_bytes(self, b: bytes, config: Config) -> None: self._cmdId, self._code = struct.unpack(" None: self._time = struct.unpack(" None: dev_id, room, unit = struct.unpack(" None: ( self._condition, self._humidity, @@ -434,7 +435,7 @@ def __init__(self, cmd_id: int, target: EnumDevice): BaseResult.__init__(self, cmd_id, target, EnumCmdType.SYS_LOGIN) self._status = None - def load_bytes(self, b): + def load_bytes(self, b: bytes, config: Config) -> None: self._status = struct.unpack(" None: self._status = struct.unpack(" None: ver_flag = 1 d = Decode(b) self._count = d.read2() @@ -490,7 +491,7 @@ def load_bytes(self, b): or EnumDevice.NEWAIRCON == device or EnumDevice.BATHROOM == device ): - dev = AirCon() + dev = AirCon(config) room.air_con = dev dev.new_air_con = EnumDevice.NEWAIRCON == device dev.bath_room = EnumDevice.BATHROOM == device @@ -526,7 +527,7 @@ def load_bytes(self, b): dev.alias = room.alias self.rooms.append(room) - def do(self): + def do(self) -> None: from .service import Service Service.set_rooms(self.rooms) @@ -582,7 +583,7 @@ def __init__(self, cmd_id: int, target: EnumDevice): self, cmd_id, target, EnumCmdType.SYS_QUERY_SCHEDULE_SETTING ) - def load_bytes(self, b): + def load_bytes(self, b: bytes, config: Config) -> None: """todo""" @@ -590,7 +591,7 @@ 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): + def load_bytes(self, b: bytes, config: Config) -> None: """todo""" @@ -599,11 +600,11 @@ def __init__(self, cmd_id: int, target: EnumDevice): BaseResult.__init__(self, cmd_id, target, EnumCmdType.SYS_HAND_SHAKE) 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) -> None: p = GetRoomInfoParam() p.room_ids.append(0xFFFF) from .service import Service @@ -617,10 +618,10 @@ def __init__(self, cmd_id: int, target: EnumDevice): BaseResult.__init__(self, cmd_id, target, EnumCmdType.SYS_HAND_SHAKE) self._time: str = "" - def load_bytes(self, b): + def load_bytes(self, b: bytes, config: Config) -> None: """todo""" - def do(self): + def do(self) -> None: """todo""" @@ -628,7 +629,7 @@ 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): + def load_bytes(self, b: bytes, config: Config) -> None: """todo""" @@ -636,7 +637,7 @@ 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): + def load_bytes(self, b: bytes, config: Config) -> None: """todo""" @@ -647,7 +648,7 @@ def __init__(self, cmd_id: int, target: EnumDevice): 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() @@ -663,13 +664,13 @@ 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): + def do(self) -> None: from .service import Service Service.update_aircon(self.target, self._room, self._unit, status=self._status) @@ -694,7 +695,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() @@ -705,7 +706,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 @@ -714,7 +715,7 @@ 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) @@ -732,7 +733,7 @@ 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) @@ -744,7 +745,7 @@ def load_bytes(self, b): if flag >> 7 & 1: self.breathe = EnumControl.Breathe(d.read1()) - def do(self): + def do(self) -> None: from .service import Service status = AirConStatus( @@ -769,7 +770,7 @@ def __init__(self, cmd_id: int, target: EnumDevice): 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() @@ -788,14 +789,14 @@ def __init__(self, cmd_id: int, target: EnumDevice): BaseResult.__init__(self, cmd_id, target, EnumCmdType.AIR_CAPABILITY_QUERY) self._air_cons: typing.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): room_id = d.read1() unit_size = d.read1() for j in range(unit_size): - aircon = AirCon() + aircon = AirCon(config) aircon.unit_id = d.read1() aircon.room_id = room_id aircon.new_air_con = self.target == EnumDevice.NEWAIRCON @@ -807,7 +808,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 @@ -836,7 +837,7 @@ def load_bytes(self, b): d.read1() self._air_cons.append(aircon) - def do(self): + def do(self) -> None: from .service import Service if Service.is_ready(): @@ -864,7 +865,7 @@ 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): + def load_bytes(self, b: bytes, config: Config) -> None: """todo""" @@ -873,7 +874,7 @@ def __init__(self, cmd_id: int, target: EnumDevice, cmd_type: EnumCmdType): BaseResult.__init__(self, cmd_id, target, cmd_type) self._subbody = "" - def load_bytes(self, b): + def load_bytes(self, b: bytes, config: Config) -> None: self._subbody = struct.pack("<" + str(len(b)) + "s", b).hex() @property diff --git a/custom_components/ds_air/ds_air_service/param.py b/custom_components/ds_air/ds_air_service/param.py index b9b547b..9eb22bc 100644 --- a/custom_components/ds_air/ds_air_service/param.py +++ b/custom_components/ds_air/ds_air_service/param.py @@ -39,7 +39,7 @@ def writes(self, d): 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) @@ -59,10 +59,10 @@ def __init__( 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 长度,不含首尾保留字及长度本身 @@ -75,7 +75,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() @@ -88,7 +88,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) @@ -118,7 +118,7 @@ def __init__(self): 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) @@ -136,7 +136,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) @@ -150,7 +150,7 @@ def __init__(self): AirconParam.__init__(self, EnumCmdType.AIR_CAPABILITY_QUERY, True) self._aircons: typing.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) @@ -176,7 +176,7 @@ def __init__(self): super().__init__(EnumCmdType.QUERY_STATUS, True) self._device: Optional[AirCon] = 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 @@ -185,7 +185,7 @@ 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 config.is_new_version: if ( dev.fan_direction1 != EnumFanDirection.FIX and dev.fan_direction2 != EnumFanDirection.FIX @@ -216,7 +216,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) @@ -238,7 +238,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 9f0392c..c73d3bf 100644 --- a/custom_components/ds_air/ds_air_service/service.py +++ b/custom_components/ds_air/ds_air_service/service.py @@ -4,6 +4,7 @@ from collections.abc import Callable from threading import Lock, Thread +from .config import Config from .ctrl_enum import EnumDevice from .dao import AirCon, AirConStatus, Room, STATUS_ATTR, Sensor, get_device_by_aircon from .decoder import BaseResult, decoder @@ -27,9 +28,10 @@ def _log(s: str): class SocketClient: - def __init__(self, host: str, port: int): + def __init__(self, host: str, port: int, config: Config): self._host = host self._port = port + self._config = config self._locker = Lock() self._s = None while not self.do_connect(): @@ -56,13 +58,13 @@ def do_connect(self): def send(self, p: Param): self._locker.acquire() - _log("send hex: 0x" + p.to_string().hex()) + _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) @@ -87,7 +89,7 @@ def recv(self) -> (list[BaseResult], bytes): _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: @@ -159,11 +161,11 @@ class Service: _scan_interval: int = 5 @staticmethod - def init(host: str, port: int, scan_interval: int): + def init(host: str, port: int, scan_interval: int, config: Config): if Service._ready: return Service._scan_interval = scan_interval - Service._socket_client = SocketClient(host, port) + Service._socket_client = SocketClient(host, port, config) Service._socket_client.send(HandShakeParam()) Service._heartbeat_thread = HeartBeatThread() Service._heartbeat_thread.start() diff --git a/tests/test.py b/tests/test.py index fd1f252..b1555ab 100644 --- a/tests/test.py +++ b/tests/test.py @@ -1,3 +1,4 @@ +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 @@ -7,18 +8,19 @@ ] -def show(s): +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 From 688b184dcd56c0a571cb6e2db2076043a48ac3fc Mon Sep 17 00:00:00 2001 From: Necroneco Date: Thu, 5 Sep 2024 14:03:42 +0800 Subject: [PATCH 11/15] move state_change_listener inner Service --- custom_components/ds_air/__init__.py | 12 ++++++------ custom_components/ds_air/climate.py | 2 +- custom_components/ds_air/ds_air_service/service.py | 2 ++ 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/custom_components/ds_air/__init__.py b/custom_components/ds_air/__init__.py index 369e473..b6f6fbd 100644 --- a/custom_components/ds_air/__init__.py +++ b/custom_components/ds_air/__init__.py @@ -30,10 +30,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: _LOGGER.debug(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 + # 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 config = Config() config.is_c611 = gw == DEFAULT_GW @@ -47,8 +47,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: 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")() + if Service.state_change_listener is not None: + Service.state_change_listener() unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) Service.destroy() diff --git a/custom_components/ds_air/climate.py b/custom_components/ds_air/climate.py index 540a376..71f7f1f 100644 --- a/custom_components/ds_air/climate.py +++ b/custom_components/ds_air/climate.py @@ -107,7 +107,7 @@ async def listener(event: Event): 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 + Service.state_change_listener = remove_listener class DsAir(ClimateEntity): diff --git a/custom_components/ds_air/ds_air_service/service.py b/custom_components/ds_air/ds_air_service/service.py index c73d3bf..331bf3f 100644 --- a/custom_components/ds_air/ds_air_service/service.py +++ b/custom_components/ds_air/ds_air_service/service.py @@ -160,6 +160,8 @@ class Service: _sensors: list[Sensor] = [] _scan_interval: int = 5 + state_change_listener: Callable[[], None] | None = None + @staticmethod def init(host: str, port: int, scan_interval: int, config: Config): if Service._ready: From 4521f328bbfa8491aedbe5823c77c86d08b6a0a7 Mon Sep 17 00:00:00 2001 From: Necroneco Date: Thu, 5 Sep 2024 14:38:54 +0800 Subject: [PATCH 12/15] make `Service` no longer a global variable --- custom_components/ds_air/__init__.py | 24 +- custom_components/ds_air/climate.py | 29 +-- custom_components/ds_air/config_flow.py | 50 ++-- .../ds_air/ds_air_service/decoder.py | 78 +++--- .../ds_air/ds_air_service/service.py | 234 ++++++++---------- custom_components/ds_air/sensor.py | 11 +- 6 files changed, 206 insertions(+), 220 deletions(-) diff --git a/custom_components/ds_air/__init__.py b/custom_components/ds_air/__init__.py index b6f6fbd..e67cd3c 100644 --- a/custom_components/ds_air/__init__.py +++ b/custom_components/ds_air/__init__.py @@ -13,6 +13,7 @@ from .const import CONF_GW, DEFAULT_GW, DEFAULT_HOST, DEFAULT_PORT, DOMAIN from .ds_air_service import Config, Service + _LOGGER = logging.getLogger(__name__) PLATFORMS = [ @@ -30,15 +31,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: _LOGGER.debug(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 - config = Config() config.is_c611 = gw == DEFAULT_GW - await hass.async_add_executor_job(Service.init, host, port, scan_interval, config) + 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)) @@ -47,12 +45,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - if Service.state_change_listener is not None: - Service.state_change_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) - Service.destroy() + if not unload_ok: + return False + + service.destroy() - return unload_ok + return True async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: diff --git a/custom_components/ds_air/climate.py b/custom_components/ds_air/climate.py index 71f7f1f..0a8172d 100644 --- a/custom_components/ds_air/climate.py +++ b/custom_components/ds_air/climate.py @@ -46,7 +46,7 @@ get_fan_direction_name, get_mode_name, ) -from .ds_air_service import AirCon, AirConStatus, Config, EnumControl, display, Service +from .ds_air_service import AirCon, AirConStatus, EnumControl, display, Service _SUPPORT_FLAGS = ( @@ -74,10 +74,10 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the climate devices.""" - + service: Service = hass.data[DOMAIN][entry.entry_id] climates = [] - for aircon in Service.get_aircons(): - climates.append(DsAir(aircon)) + for aircon in service.get_aircons(): + climates.append(DsAir(service, aircon)) async_add_entities(climates) link = entry.options.get("link") sensor_temp_map: dict[str, list[DsAir]] = {} @@ -107,7 +107,7 @@ async def listener(event: Event): 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 + service.state_change_listener = remove_listener class DsAir(ClimateEntity): @@ -133,18 +133,19 @@ class DsAir(ClimateEntity): _enable_turn_on_off_backwards_compatibility: bool = False # used in 2024.2~2024.12 - def __init__(self, aircon: AirCon): + def __init__(self, service: Service, aircon: AirCon): _log("create aircon:") _log(str(aircon.__dict__)) _log(str(aircon.status.__dict__)) """Initialize the climate device.""" + self.service = service self._device_info = aircon 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 - 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)}, @@ -314,7 +315,7 @@ def set_temperature(self, **kwargs) -> None: new_status = AirConStatus() status.setted_temp = round(temperate * 10.0) new_status.setted_temp = round(temperate * 10.0) - Service.control(self._device_info, new_status) + self.service.control(self._device_info, new_status) self.schedule_update_ha_state() def set_humidity(self, humidity: int) -> None: @@ -327,7 +328,7 @@ def set_humidity(self, humidity: int) -> None: new_status = AirConStatus() status.humidity = EnumControl.Humidity(humidity) new_status.humidity = EnumControl.Humidity(humidity) - Service.control(self._device_info, new_status) + self.service.control(self._device_info, new_status) self.schedule_update_ha_state() def set_fan_mode(self, fan_mode: str) -> None: @@ -340,7 +341,7 @@ def set_fan_mode(self, fan_mode: str) -> None: new_status = AirConStatus() status.air_flow = get_air_flow_enum(fan_mode) new_status.air_flow = get_air_flow_enum(fan_mode) - Service.control(self._device_info, new_status) + self.service.control(self._device_info, new_status) self.schedule_update_ha_state() def set_hvac_mode(self, hvac_mode: HVACMode) -> None: @@ -351,7 +352,7 @@ def set_hvac_mode(self, hvac_mode: HVACMode) -> None: if hvac_mode == HVACMode.OFF: status.switch = EnumControl.Switch.OFF new_status.switch = EnumControl.Switch.OFF - 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 @@ -382,7 +383,7 @@ def set_hvac_mode(self, hvac_mode: HVACMode) -> None: mode = m.SLEEP status.mode = mode new_status.mode = mode - 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: str) -> None: @@ -394,7 +395,7 @@ def set_swing_mode(self, swing_mode: str) -> None: new_status.fan_direction1 = self._device_info.status.fan_direction1 status.fan_direction2 = get_fan_direction_enum(swing_mode) new_status.fan_direction2 = get_fan_direction_enum(swing_mode) - Service.control(self._device_info, new_status) + self.service.control(self._device_info, new_status) self.schedule_update_ha_state() def set_preset_mode(self, preset_mode: str) -> None: @@ -418,7 +419,7 @@ def set_preset_mode(self, preset_mode: str) -> None: mode = m.RELAX status.mode = mode new_status.mode = mode - Service.control(self._device_info, new_status) + self.service.control(self._device_info, new_status) self.schedule_update_ha_state() @property diff --git a/custom_components/ds_air/config_flow.py b/custom_components/ds_air/config_flow.py index 4650b26..4819599 100644 --- a/custom_components/ds_air/config_flow.py +++ b/custom_components/ds_air/config_flow.py @@ -18,7 +18,6 @@ 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 import Service _LOGGER = logging.getLogger(__name__) @@ -38,30 +37,27 @@ def __init__(self): 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") - errors = {} if user_input is not None: self.user_input.update(user_input) 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", @@ -79,7 +75,7 @@ async def async_step_user( @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) @@ -91,10 +87,10 @@ def __init__(self, config_entry: ConfigEntry) -> None: """Initialize options flow.""" self.config_entry = config_entry self._config_data = [] - self._climates = list(map(lambda state: state.alias, Service.get_aircons())) - self._sensors_temp = {} - self._sensors_humi = {} - self._len = len(self._climates) + 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 = {} @@ -102,6 +98,10 @@ 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 = list(map(lambda state: state.alias, service.get_aircons())) + self._len = len(self._climates) + sensors = self.hass.states.async_all("sensor") self._sensors_temp = { None: "None", diff --git a/custom_components/ds_air/ds_air_service/decoder.py b/custom_components/ds_air/ds_air_service/decoder.py index 7e0f33c..2436ba3 100644 --- a/custom_components/ds_air/ds_air_service/decoder.py +++ b/custom_components/ds_air/ds_air_service/decoder.py @@ -1,5 +1,7 @@ +from __future__ import annotations + import struct -import typing +from typing import TYPE_CHECKING from .base_bean import BaseBean from .config import Config @@ -34,6 +36,9 @@ Sensor2InfoParam, ) +if TYPE_CHECKING: + from .service import Service + def decoder(b: bytes, config: Config): if b[0] != 2: @@ -187,7 +192,7 @@ def __init__(self, cmd_id: int, targe: EnumDevice, cmd_type: EnumCmdType): def load_bytes(self, b: bytes, config: Config) -> None: """do nothing""" - def do(self) -> None: + def do(self, service: Service) -> None: """do nothing""" @@ -216,7 +221,7 @@ def __init__(self, cmd_id: int, target: EnumDevice): self._mode = 0 self._room_id = 0 self._sensor_type = 0 - self._sensors: typing.List[Sensor] = [] + self._sensors: list[Sensor] = [] def load_bytes(self, b: bytes, config: Config) -> None: data = Decode(b) @@ -302,10 +307,8 @@ def load_bytes(self, b: bytes, config: Config) -> None: self._sensors.append(sensor) count = count - 1 - def do(self) -> None: - 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): @@ -460,9 +463,9 @@ class GetRoomInfoResult(BaseResult): def __init__(self, cmd_id: int, target: EnumDevice): BaseResult.__init__(self, cmd_id, target, EnumCmdType.SYS_GET_ROOM_INFO) self._count: int = 0 - self._hds: typing.List[HD] = [] - self._sensors: typing.List[Sensor] = [] - self._rooms: typing.List[Room] = [] + self._hds: list[HD] = [] + self._sensors: list[Sensor] = [] + self._rooms: list[Room] = [] def load_bytes(self, b: bytes, config: Config) -> None: ver_flag = 1 @@ -527,17 +530,15 @@ def load_bytes(self, b: bytes, config: Config) -> None: dev.alias = room.alias self.rooms.append(room) - def do(self) -> None: - 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: @@ -550,15 +551,15 @@ def do(self) -> None: 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): @@ -604,13 +605,12 @@ def load_bytes(self, b: bytes, config: Config) -> None: d = Decode(b) self._time = d.read_utf(14) - def do(self) -> None: + 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()) + service.send_msg(p) + service.send_msg(Sensor2InfoParam()) class GetGWInfoResult(BaseResult): @@ -621,7 +621,7 @@ def __init__(self, cmd_id: int, target: EnumDevice): def load_bytes(self, b: bytes, config: Config) -> None: """todo""" - def do(self) -> None: + def do(self, service: Service) -> None: """todo""" @@ -670,10 +670,8 @@ def load_bytes(self, b: bytes, config: Config) -> None: status.fan_direction1 = EnumControl.FanDirection(direction & 0xF) status.fan_direction2 = EnumControl.FanDirection((direction >> 4) & 0xF) - def do(self) -> None: - 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): @@ -745,9 +743,7 @@ def load_bytes(self, b: bytes, config: Config) -> None: if flag >> 7 & 1: self.breathe = EnumControl.Breathe(d.read1()) - def do(self) -> None: - from .service import Service - + def do(self, service: Service) -> None: status = AirConStatus( self.current_temp, self.setted_temp, @@ -759,7 +755,7 @@ def do(self) -> None: self.humidity, self.mode, ) - Service.set_aircon_status(self.target, self.room, self.unit, status) + service.set_aircon_status(self.target, self.room, self.unit, status) class AirConRecommendedIndoorTempResult(BaseResult): @@ -787,7 +783,7 @@ 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: bytes, config: Config) -> None: d = Decode(b) @@ -837,13 +833,11 @@ def load_bytes(self, b: bytes, config: Config) -> None: d.read1() self._air_cons.append(aircon) - def do(self) -> None: - 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( + service.update_aircon( get_device_by_aircon(i), i.room_id, i.unit_id, aircon=i ) else: @@ -851,10 +845,8 @@ def do(self) -> None: 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): diff --git a/custom_components/ds_air/ds_air_service/service.py b/custom_components/ds_air/ds_air_service/service.py index 331bf3f..98970aa 100644 --- a/custom_components/ds_air/ds_air_service/service.py +++ b/custom_components/ds_air/ds_air_service/service.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import logging import socket import time @@ -28,7 +30,7 @@ def _log(s: str): class SocketClient: - def __init__(self, host: str, port: int, config: Config): + def __init__(self, host: str, port: int, service: Service, config: Config): self._host = host self._port = port self._config = config @@ -37,7 +39,7 @@ def __init__(self, host: str, port: int, config: Config): 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): @@ -99,9 +101,10 @@ def recv(self) -> (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 @@ -117,15 +120,16 @@ def run(self) -> None: 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): @@ -136,175 +140,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: SocketClient = None - _rooms: list[Room] = None - _aircons: list[AirCon] = None - _new_aircons: list[AirCon] = None - _bathrooms: list[AirCon] = None - _ready: bool = False - _none_stat_dev_cnt: int = 0 - _status_hook: list[(AirCon, Callable)] = [] - _sensor_hook: list[(str, Callable)] = [] - _heartbeat_thread = None - _sensors: list[Sensor] = [] - _scan_interval: int = 5 - - state_change_listener: Callable[[], None] | None = None - - @staticmethod - def init(host: str, port: int, scan_interval: int, config: Config): - 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, config) - Service._socket_client.send(HandShakeParam()) - Service._heartbeat_thread = HeartBeatThread() - Service._heartbeat_thread.start() + 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 ( - Service._rooms is None - or Service._aircons is None - or Service._new_aircons is None - or Service._bathrooms is None + 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() -> list[AirCon]: + 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: 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: 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): + def send_msg(self, p: Param): """send msg to climate gateway""" - Service._socket_client.send(p) + self._socket_client.send(p) - @staticmethod - def get_rooms(): - return Service._rooms + def get_rooms(self): + return self._rooms - @staticmethod - def set_rooms(v: 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: 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 + self, target: EnumDevice, room: int, unit: int, status: AirConStatus ): - if Service._ready: - Service.update_aircon(target, room, unit, status=status) + 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: 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: @@ -312,19 +301,17 @@ def set_sensors_status(sensors: 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 ( @@ -338,6 +325,5 @@ def update_aircon(target: EnumDevice, room: int, unit: int, **kwargs): _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/sensor.py b/custom_components/ds_air/sensor.py index 325773e..5f7de75 100644 --- a/custom_components/ds_air/sensor.py +++ b/custom_components/ds_air/sensor.py @@ -17,11 +17,12 @@ async def async_setup_entry( 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 device in service.get_sensors(): for key in SENSOR_DESCRIPTORS: if config_entry.data.get(key): - entities.append(DsSensor(device, SENSOR_DESCRIPTORS.get(key))) + entities.append(DsSensor(service, device, SENSOR_DESCRIPTORS.get(key))) async_add_entities(entities) @@ -32,7 +33,9 @@ class DsSensor(SensorEntity): _attr_should_poll: bool = False - def __init__(self, device: Sensor, description: DsSensorEntityDescription): + def __init__( + self, service: Service, device: Sensor, description: DsSensorEntityDescription + ): """Initialize the Daikin Sensor.""" self.entity_description = description self._data_key: str = description.key @@ -47,7 +50,7 @@ def __init__(self, device: Sensor, description: DsSensorEntityDescription): 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) + service.register_sensor_hook(device.unique_id, self._handle_sensor_hook) def _parse_data(self, device: Sensor) -> None: """Parse data sent by gateway.""" From a1ad613349971d87b2d18df55e932593199568df Mon Sep 17 00:00:00 2001 From: Necroneco Date: Thu, 5 Sep 2024 14:58:37 +0800 Subject: [PATCH 13/15] ruff check fix --- custom_components/ds_air/__init__.py | 4 +- custom_components/ds_air/climate.py | 33 ++++++------- custom_components/ds_air/config_flow.py | 1 + custom_components/ds_air/const.py | 1 - custom_components/ds_air/descriptions.py | 3 +- .../ds_air/ds_air_service/__init__.py | 2 +- .../ds_air/ds_air_service/dao.py | 9 ++-- .../ds_air/ds_air_service/decoder.py | 49 +++++++++---------- .../ds_air/ds_air_service/param.py | 12 ++--- .../ds_air/ds_air_service/service.py | 10 ++-- custom_components/ds_air/sensor.py | 4 +- 11 files changed, 59 insertions(+), 69 deletions(-) diff --git a/custom_components/ds_air/__init__.py b/custom_components/ds_air/__init__.py index e67cd3c..4198211 100644 --- a/custom_components/ds_air/__init__.py +++ b/custom_components/ds_air/__init__.py @@ -1,5 +1,4 @@ -""" -Platform for DS-AIR of Daikin +"""Platform for DS-AIR of Daikin https://www.daikin-china.com.cn/newha/products/4/19/DS-AIR/ """ @@ -13,7 +12,6 @@ from .const import CONF_GW, DEFAULT_GW, DEFAULT_HOST, DEFAULT_PORT, DOMAIN from .ds_air_service import Config, Service - _LOGGER = logging.getLogger(__name__) PLATFORMS = [ diff --git a/custom_components/ds_air/climate.py b/custom_components/ds_air/climate.py index 0a8172d..72a4098 100644 --- a/custom_components/ds_air/climate.py +++ b/custom_components/ds_air/climate.py @@ -1,5 +1,4 @@ -""" -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/ @@ -8,15 +7,16 @@ import logging import voluptuous as vol + from homeassistant.components.climate import ( - ClimateEntity, - ClimateEntityFeature, - HVACAction, - HVACMode, PLATFORM_SCHEMA, PRESET_COMFORT, PRESET_NONE, PRESET_SLEEP, + ClimateEntity, + ClimateEntityFeature, + HVACAction, + HVACMode, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -46,8 +46,7 @@ get_fan_direction_name, get_mode_name, ) -from .ds_air_service import AirCon, AirConStatus, EnumControl, display, Service - +from .ds_air_service import AirCon, AirConStatus, EnumControl, Service, display _SUPPORT_FLAGS = ( ClimateEntityFeature.TARGET_TEMPERATURE @@ -230,7 +229,7 @@ def hvac_mode(self) -> HVACMode | None: @property def hvac_modes(self) -> list[HVACMode]: """Return the list of available hvac operation modes.""" - li = [] + li: list[HVACMode] = [] aircon = self._device_info if aircon.cool_mode: li.append(HVACMode.COOL) @@ -250,11 +249,10 @@ def current_temperature(self) -> float | None: """Return the current temperature.""" if self._link_cur_temp: return self._attr_current_temperature + elif self._device_info.config.is_c611: + return None else: - if self._device_info.config.is_c611: - return None - else: - return self._device_info.status.current_temp / 10 + return self._device_info.status.current_temp / 10 @property def target_temperature(self) -> float | None: @@ -412,11 +410,10 @@ 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 self.service.control(self._device_info, new_status) diff --git a/custom_components/ds_air/config_flow.py b/custom_components/ds_air/config_flow.py index 4819599..a73cc3e 100644 --- a/custom_components/ds_air/config_flow.py +++ b/custom_components/ds_air/config_flow.py @@ -4,6 +4,7 @@ from typing import Any import voluptuous as vol + from homeassistant.components.sensor import SensorDeviceClass from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow from homeassistant.const import ( diff --git a/custom_components/ds_air/const.py b/custom_components/ds_air/const.py index ef6519f..63eb105 100644 --- a/custom_components/ds_air/const.py +++ b/custom_components/ds_air/const.py @@ -9,7 +9,6 @@ from .ds_air_service import EnumControl - DOMAIN = "ds_air" CONF_GW = "gw" DEFAULT_HOST = "192.168.1." diff --git a/custom_components/ds_air/descriptions.py b/custom_components/ds_air/descriptions.py index 8e78678..758c44a 100644 --- a/custom_components/ds_air/descriptions.py +++ b/custom_components/ds_air/descriptions.py @@ -1,5 +1,6 @@ +from collections.abc import Callable from dataclasses import dataclass -from typing import Any, Callable +from typing import Any from homeassistant.components.sensor import ( SensorDeviceClass, diff --git a/custom_components/ds_air/ds_air_service/__init__.py b/custom_components/ds_air/ds_air_service/__init__.py index 391a982..18f7365 100644 --- a/custom_components/ds_air/ds_air_service/__init__.py +++ b/custom_components/ds_air/ds_air_service/__init__.py @@ -1,5 +1,5 @@ from .config import Config from .ctrl_enum import EnumControl -from .dao import AirCon, AirConStatus, Sensor, UNINITIALIZED_VALUE +from .dao import UNINITIALIZED_VALUE, AirCon, AirConStatus, Sensor from .display import display from .service import Service diff --git a/custom_components/ds_air/ds_air_service/dao.py b/custom_components/ds_air/ds_air_service/dao.py index 26ef804..26f90f4 100644 --- a/custom_components/ds_air/ds_air_service/dao.py +++ b/custom_components/ds_air/ds_air_service/dao.py @@ -1,6 +1,6 @@ import time -from typing import Optional +from .config import Config from .ctrl_enum import ( EnumControl, EnumDevice, @@ -9,7 +9,6 @@ EnumOutDoorRunCond, EnumSwitch, ) -from .config import Config class Device: @@ -175,12 +174,12 @@ class Room: def __init__(self): self.air_con = None self.alias: str = "" - self.geothermic: Optional[Geothermic] = None - self.hd: Optional[HD] = None + 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: Optional[Ventilation] = Ventilation() + 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 2436ba3..6b0b80b 100644 --- a/custom_components/ds_air/ds_air_service/decoder.py +++ b/custom_components/ds_air/ds_air_service/decoder.py @@ -17,14 +17,14 @@ ThreeDFresh, ) from .dao import ( + HD, + UNINITIALIZED_VALUE, AirCon, AirConStatus, Device, Geothermic, - HD, Room, Sensor, - UNINITIALIZED_VALUE, Ventilation, get_device_by_aircon, ) @@ -190,10 +190,10 @@ def __init__(self, cmd_id: int, targe: EnumDevice, cmd_type: EnumCmdType): BaseBean.__init__(self, cmd_id, targe, cmd_type) def load_bytes(self, b: bytes, config: Config) -> None: - """do nothing""" + """Do nothing""" def do(self, service: Service) -> None: - """do nothing""" + """Do nothing""" class HeartbeatResult(BaseResult): @@ -490,33 +490,33 @@ def load_bytes(self, b: bytes, config: Config) -> None: device_count = d.read2() for unit_id in range(device_count): if ( - EnumDevice.AIRCON == device - or EnumDevice.NEWAIRCON == device - or EnumDevice.BATHROOM == device + device == EnumDevice.AIRCON + or device == EnumDevice.NEWAIRCON + or device == 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 + device == EnumDevice.VENTILATION + or device == 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 @@ -585,7 +585,7 @@ def __init__(self, cmd_id: int, target: EnumDevice): ) def load_bytes(self, b: bytes, config: Config) -> None: - """todo""" + """Todo""" class QueryScheduleIDResult(BaseResult): @@ -593,7 +593,7 @@ def __init__(self, cmd_id: int, target: EnumDevice): BaseResult.__init__(self, cmd_id, target, EnumCmdType.SYS_QUERY_SCHEDULE_ID) def load_bytes(self, b: bytes, config: Config) -> None: - """todo""" + """Todo""" class HandShakeResult(BaseResult): @@ -619,10 +619,10 @@ def __init__(self, cmd_id: int, target: EnumDevice): self._time: str = "" def load_bytes(self, b: bytes, config: Config) -> None: - """todo""" + """Todo""" def do(self, service: Service) -> None: - """todo""" + """Todo""" class CmdTransferResult(BaseResult): @@ -630,7 +630,7 @@ def __init__(self, cmd_id: int, target: EnumDevice): BaseResult.__init__(self, cmd_id, target, EnumCmdType.SYS_CMD_TRANSFER) def load_bytes(self, b: bytes, config: Config) -> None: - """todo""" + """Todo""" class QueryScheduleFinish(BaseResult): @@ -638,7 +638,7 @@ def __init__(self, cmd_id: int, target: EnumDevice): BaseResult.__init__(self, cmd_id, target, EnumCmdType.SYS_QUERY_SCHEDULE_FINISH) def load_bytes(self, b: bytes, config: Config) -> None: - """todo""" + """Todo""" class AirConStatusChangedResult(BaseResult): @@ -739,9 +739,8 @@ def load_bytes(self, b: bytes, config: Config) -> None: 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()) + elif flag >> 7 & 1: + self.breathe = EnumControl.Breathe(d.read1()) def do(self, service: Service) -> None: status = AirConStatus( @@ -858,7 +857,7 @@ def __init__(self, cmd_id: int, target: EnumDevice): BaseResult.__init__(self, cmd_id, target, EnumCmdType.QUERY_SCENARIO_SETTING) def load_bytes(self, b: bytes, config: Config) -> None: - """todo""" + """Todo""" class UnknownResult(BaseResult): diff --git a/custom_components/ds_air/ds_air_service/param.py b/custom_components/ds_air/ds_air_service/param.py index 9eb22bc..edb21ec 100644 --- a/custom_components/ds_air/ds_air_service/param.py +++ b/custom_components/ds_air/ds_air_service/param.py @@ -1,6 +1,4 @@ import struct -import typing -from typing import Optional from .base_bean import BaseBean from .config import Config @@ -114,7 +112,7 @@ 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 @@ -148,7 +146,7 @@ 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: Encode, config: Config) -> None: s.write1(len(self._aircons)) @@ -174,7 +172,7 @@ def __init__(self): class AirConQueryStatusParam(AirconParam): def __init__(self): super().__init__(EnumCmdType.QUERY_STATUS, True) - self._device: Optional[AirCon] = None + self._device: AirCon | None = None def generate_subbody(self, s: Encode, config: Config) -> None: s.write1(self._device.room_id) @@ -191,9 +189,7 @@ def generate_subbody(self, s: Encode, config: Config) -> None: and dev.fan_direction2 != EnumFanDirection.FIX ): 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: diff --git a/custom_components/ds_air/ds_air_service/service.py b/custom_components/ds_air/ds_air_service/service.py index 98970aa..a53445d 100644 --- a/custom_components/ds_air/ds_air_service/service.py +++ b/custom_components/ds_air/ds_air_service/service.py @@ -1,14 +1,14 @@ from __future__ import annotations +from collections.abc import Callable import logging import socket -import time -from collections.abc import Callable from threading import Lock, Thread +import time from .config import Config from .ctrl_enum import EnumDevice -from .dao import AirCon, AirConStatus, Room, STATUS_ATTR, Sensor, get_device_by_aircon +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 ( @@ -53,7 +53,7 @@ def do_connect(self): self._s.connect((self._host, self._port)) _log("connected") return True - except socket.error as exc: + except OSError as exc: _log("connected error") _log(str(exc)) return False @@ -243,7 +243,7 @@ def is_ready(self) -> bool: return self._ready def send_msg(self, p: Param): - """send msg to climate gateway""" + """Send msg to climate gateway""" self._socket_client.send(p) def get_rooms(self): diff --git a/custom_components/ds_air/sensor.py b/custom_components/ds_air/sensor.py index 5f7de75..947feca 100644 --- a/custom_components/ds_air/sensor.py +++ b/custom_components/ds_air/sensor.py @@ -7,8 +7,8 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN, MANUFACTURER -from .descriptions import DsSensorEntityDescription, SENSOR_DESCRIPTORS -from .ds_air_service import Sensor, Service, UNINITIALIZED_VALUE +from .descriptions import SENSOR_DESCRIPTORS, DsSensorEntityDescription +from .ds_air_service import UNINITIALIZED_VALUE, Sensor, Service async def async_setup_entry( From a65276e2d043e8dfa71ff885ff7de2878172c173 Mon Sep 17 00:00:00 2001 From: Necroneco Date: Thu, 5 Sep 2024 15:20:21 +0800 Subject: [PATCH 14/15] ruff check fix unsafe --- custom_components/ds_air/__init__.py | 5 +- custom_components/ds_air/climate.py | 9 ++-- custom_components/ds_air/config_flow.py | 2 +- .../ds_air/ds_air_service/__init__.py | 11 +++++ .../ds_air/ds_air_service/ctrl_enum.py | 8 ++-- .../ds_air/ds_air_service/dao.py | 41 ++++++++--------- .../ds_air/ds_air_service/decoder.py | 46 +++++++++---------- .../ds_air/ds_air_service/display.py | 24 +++++----- .../ds_air/ds_air_service/param.py | 5 +- .../ds_air/ds_air_service/service.py | 5 +- 10 files changed, 80 insertions(+), 76 deletions(-) diff --git a/custom_components/ds_air/__init__.py b/custom_components/ds_air/__init__.py index 4198211..4a45f0e 100644 --- a/custom_components/ds_air/__init__.py +++ b/custom_components/ds_air/__init__.py @@ -1,4 +1,5 @@ """Platform for DS-AIR of Daikin + https://www.daikin-china.com.cn/newha/products/4/19/DS-AIR/ """ @@ -9,7 +10,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceEntry -from .const import CONF_GW, DEFAULT_GW, DEFAULT_HOST, DEFAULT_PORT, DOMAIN +from .const import CONF_GW, DEFAULT_GW, DOMAIN from .ds_air_service import Config, Service _LOGGER = logging.getLogger(__name__) @@ -27,7 +28,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: gw = entry.data[CONF_GW] scan_interval = entry.data[CONF_SCAN_INTERVAL] - _LOGGER.debug(f"{host}:{port} {gw} {scan_interval}") + _LOGGER.debug("%s:%s %s %s", host, port, gw, scan_interval) config = Config() config.is_c611 = gw == DEFAULT_GW diff --git a/custom_components/ds_air/climate.py b/custom_components/ds_air/climate.py index 72a4098..6d10990 100644 --- a/custom_components/ds_air/climate.py +++ b/custom_components/ds_air/climate.py @@ -74,9 +74,7 @@ async def async_setup_entry( ) -> None: """Set up the climate devices.""" service: Service = hass.data[DOMAIN][entry.entry_id] - climates = [] - for aircon in service.get_aircons(): - climates.append(DsAir(service, aircon)) + 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]] = {} @@ -249,10 +247,9 @@ def current_temperature(self) -> float | None: """Return the current temperature.""" if self._link_cur_temp: return self._attr_current_temperature - elif self._device_info.config.is_c611: + if self._device_info.config.is_c611: return None - else: - return self._device_info.status.current_temp / 10 + return self._device_info.status.current_temp / 10 @property def target_temperature(self) -> float | None: diff --git a/custom_components/ds_air/config_flow.py b/custom_components/ds_air/config_flow.py index a73cc3e..b609a04 100644 --- a/custom_components/ds_air/config_flow.py +++ b/custom_components/ds_air/config_flow.py @@ -100,7 +100,7 @@ async def async_step_init( ) -> FlowResult: """Manage the options.""" service = self.hass.data[DOMAIN][self.config_entry.entry_id] - self._climates = list(map(lambda state: state.alias, service.get_aircons())) + self._climates = [state.alias for state in service.get_aircons()] self._len = len(self._climates) sensors = self.hass.states.async_all("sensor") diff --git a/custom_components/ds_air/ds_air_service/__init__.py b/custom_components/ds_air/ds_air_service/__init__.py index 18f7365..9db2f5e 100644 --- a/custom_components/ds_air/ds_air_service/__init__.py +++ b/custom_components/ds_air/ds_air_service/__init__.py @@ -3,3 +3,14 @@ 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/ctrl_enum.py b/custom_components/ds_air/ds_air_service/ctrl_enum.py index 9dc6a08..33d50b4 100644 --- a/custom_components/ds_air/ds_air_service/ctrl_enum.py +++ b/custom_components/ds_air/ds_air_service/ctrl_enum.py @@ -305,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 26f90f4..ceb429f 100644 --- a/custom_components/ds_air/ds_air_service/dao.py +++ b/custom_components/ds_air/ds_air_service/dao.py @@ -29,25 +29,25 @@ def unique_id(self): 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, + 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 = current_temp - self.setted_temp: int = setted_temp - self.switch: EnumControl.Switch = switch - self.air_flow: EnumControl.AirFlow = air_flow - self.breathe: EnumControl.Breathe = breathe - self.fan_direction1: EnumControl.FanDirection = fan_direction1 - self.fan_direction2: EnumControl.FanDirection = fan_direction2 - self.humidity: EnumControl.Humidity = humidity - self.mode: EnumControl.Mode = mode + 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): @@ -81,10 +81,9 @@ def __init__(self, config: Config): 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): diff --git a/custom_components/ds_air/ds_air_service/decoder.py b/custom_components/ds_air/ds_air_service/decoder.py index 6b0b80b..ef3802a 100644 --- a/custom_components/ds_air/ds_air_service/decoder.py +++ b/custom_components/ds_air/ds_air_service/decoder.py @@ -52,8 +52,7 @@ def decoder(b: bytes, config: Config): ): if length == 0: return HeartbeatResult(), None - else: - return None, None + return None, None return result_factory( struct.unpack(" None: 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: @@ -485,14 +484,14 @@ def load_bytes(self, b: bytes, config: Config) -> None: 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 ( - device == EnumDevice.AIRCON - or device == EnumDevice.NEWAIRCON - or device == EnumDevice.BATHROOM + if device in ( + EnumDevice.AIRCON, + EnumDevice.NEWAIRCON, + EnumDevice.BATHROOM, ): dev = AirCon(config) room.air_con = dev @@ -510,10 +509,7 @@ def load_bytes(self, b: bytes, config: Config) -> None: dev = Sensor() self.sensors.append(dev) room.sensor_room = True - elif ( - device == EnumDevice.VENTILATION - or device == EnumDevice.SMALL_VAM - ): + elif device in (EnumDevice.VENTILATION, EnumDevice.SMALL_VAM): dev = Ventilation() room.ventilation = dev dev.is_small_vam = device == EnumDevice.SMALL_VAM @@ -787,10 +783,10 @@ def __init__(self, cmd_id: int, target: EnumDevice): 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): + for _j in range(unit_size): aircon = AirCon(config) aircon.unit_id = d.read1() aircon.room_id = room_id diff --git a/custom_components/ds_air/ds_air_service/display.py b/custom_components/ds_air/ds_air_service/display.py index e46a327..1facd8c 100644 --- a/custom_components/ds_air/ds_air_service/display.py +++ b/custom_components/ds_air/ds_air_service/display.py @@ -2,21 +2,23 @@ def display(o, d="") -> str: - if type(o) == int or type(o) == str or type(o) == bool or type(o) == float: + 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: + + 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 += "}" - 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 edb21ec..384e69d 100644 --- a/custom_components/ds_air/ds_air_service/param.py +++ b/custom_components/ds_air/ds_air_service/param.py @@ -184,10 +184,7 @@ def generate_subbody(self, s: Encode, config: Config) -> 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 EnumFanDirection.FIX not in (dev.fan_direction1, dev.fan_direction2): flag = flag | t.FAN_DIRECTION if dev.bath_room or dev.three_d_fresh_allow: flag = flag | t.BREATHE diff --git a/custom_components/ds_air/ds_air_service/service.py b/custom_components/ds_air/ds_air_service/service.py index a53445d..90be93c 100644 --- a/custom_components/ds_air/ds_air_service/service.py +++ b/custom_components/ds_air/ds_air_service/service.py @@ -51,12 +51,13 @@ 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 OSError as exc: _log("connected error") _log(str(exc)) return False + else: + _log("connected") + return True def send(self, p: Param): self._locker.acquire() From fb1f4a70bf1fe946b0ec6da980c7a69f10a8a4ad Mon Sep 17 00:00:00 2001 From: Necroneco Date: Thu, 5 Sep 2024 17:55:52 +0800 Subject: [PATCH 15/15] bump version to 2.0.0 --- custom_components/ds_air/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/ds_air/manifest.json b/custom_components/ds_air/manifest.json index c12bab2..7c8463a 100644 --- a/custom_components/ds_air/manifest.json +++ b/custom_components/ds_air/manifest.json @@ -8,5 +8,5 @@ "integration_type": "hub", "issue_tracker": "https://github.com/mypal/ha-dsair/issues", "requirements": [], - "version": "1.3.5" + "version": "2.0.0" }