From 86a90a6f0e55ef19cab628ab81c9cda8a79895f1 Mon Sep 17 00:00:00 2001 From: Necroneco Date: Thu, 4 Jan 2024 11:12:44 +0800 Subject: [PATCH 1/4] Replace deprecated constants --- custom_components/ds_air/climate.py | 16 +++----------- custom_components/ds_air/const.py | 4 ++-- .../ds_air/ds_air_service/ctrl_enum.py | 22 +++++++++++-------- 3 files changed, 18 insertions(+), 24 deletions(-) diff --git a/custom_components/ds_air/climate.py b/custom_components/ds_air/climate.py index 382c273..01e6d0f 100644 --- a/custom_components/ds_air/climate.py +++ b/custom_components/ds_air/climate.py @@ -9,18 +9,8 @@ from typing import Optional, List import voluptuous as vol -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate import PLATFORM_SCHEMA -""" from homeassistant.components.climate.const import ( - SUPPORT_TARGET_TEMPERATURE, - SUPPORT_FAN_MODE, - SUPPORT_SWING_MODE, - SUPPORT_TARGET_HUMIDITY, - HVAC_MODE_OFF, HVAC_MODE_HEAT, HVAC_MODE_COOL, HVAC_MODE_HEAT_COOL, HVAC_MODE_AUTO, - HVAC_MODE_DRY, - HVAC_MODE_FAN_ONLY, - FAN_AUTO, FAN_LOW, FAN_MEDIUM, FAN_HIGH) """ from homeassistant.components.climate import ( + PLATFORM_SCHEMA, ClimateEntity, ClimateEntityFeature, HVACMode, HVACAction, @@ -28,7 +18,7 @@ FAN_AUTO, FAN_LOW, FAN_MEDIUM, FAN_HIGH ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import TEMP_CELSIUS, ATTR_TEMPERATURE, CONF_HOST, CONF_PORT +from homeassistant.const import UnitOfTemperature, ATTR_TEMPERATURE, CONF_HOST, CONF_PORT from homeassistant.core import HomeAssistant, Event from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity import DeviceInfo @@ -184,7 +174,7 @@ def name(self): @property def temperature_unit(self): """Return the unit of measurement.""" - return TEMP_CELSIUS + return UnitOfTemperature.CELSIUS @property def target_humidity(self): diff --git a/custom_components/ds_air/const.py b/custom_components/ds_air/const.py index 8ca56f8..87e9218 100644 --- a/custom_components/ds_air/const.py +++ b/custom_components/ds_air/const.py @@ -1,4 +1,4 @@ -from homeassistant.const import TEMP_CELSIUS, PERCENTAGE, CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, \ +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 @@ -11,7 +11,7 @@ DEFAULT_GW = "DTA117C611" GW_LIST = ["DTA117C611", "DTA117B611"] SENSOR_TYPES = { - "temp": [TEMP_CELSIUS, None, SensorDeviceClass.TEMPERATURE, 10], + "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], 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 a0212d4..87b8220 100644 --- a/custom_components/ds_air/ds_air_service/ctrl_enum.py +++ b/custom_components/ds_air/ds_air_service/ctrl_enum.py @@ -1,9 +1,13 @@ from enum import Enum, IntEnum -from homeassistant.components.climate.const import \ - HVAC_MODE_COOL, HVAC_MODE_FAN_ONLY, HVAC_MODE_HEAT, HVAC_MODE_DRY, HVAC_MODE_AUTO, HVAC_MODE_HEAT_COOL, \ - FAN_AUTO, FAN_LOW, FAN_MEDIUM, FAN_HIGH, \ - HVACAction +from homeassistant.components.climate.const import ( + FAN_AUTO, + FAN_HIGH, + FAN_LOW, + FAN_MEDIUM, + HVACAction, + HVACMode, +) class EnumCmdType(IntEnum): @@ -293,11 +297,11 @@ class Mode(IntEnum): MOREDRY = 9 # Legacy Mode Mapping -#_MODE_NAME_LIST = [HVAC_MODE_COOL, HVAC_MODE_DRY, HVAC_MODE_FAN_ONLY, HVAC_MODE_AUTO, HVAC_MODE_HEAT, -# HVAC_MODE_DRY, HVAC_MODE_AUTO, HVAC_MODE_HEAT_COOL, HVAC_MODE_HEAT, HVAC_MODE_DRY] +#_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 = [HVAC_MODE_COOL, HVAC_MODE_DRY, HVAC_MODE_FAN_ONLY, HVAC_MODE_AUTO, HVAC_MODE_HEAT, - HVAC_MODE_DRY, HVAC_MODE_AUTO, HVAC_MODE_AUTO, HVAC_MODE_HEAT, HVAC_MODE_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] @@ -333,7 +337,7 @@ class EnumControl: @staticmethod def get_mode_name(idx): return _MODE_NAME_LIST[idx] - + def get_action_name(idx): return _MODE_ACTION_LIST[idx] From a0ea8abd601da3a571c57a7dcb41d7f8c1fc27e2 Mon Sep 17 00:00:00 2001 From: Necroneco Date: Fri, 9 Feb 2024 22:42:21 +0800 Subject: [PATCH 2/4] Add ClimateEntityFeature: TURN_ON, TURN_OFF --- custom_components/ds_air/climate.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/custom_components/ds_air/climate.py b/custom_components/ds_air/climate.py index 01e6d0f..2c5e9fb 100644 --- a/custom_components/ds_air/climate.py +++ b/custom_components/ds_air/climate.py @@ -18,7 +18,7 @@ FAN_AUTO, FAN_LOW, FAN_MEDIUM, FAN_HIGH ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import UnitOfTemperature, ATTR_TEMPERATURE, CONF_HOST, CONF_PORT +from homeassistant.const import MAJOR_VERSION, MINOR_VERSION, UnitOfTemperature, ATTR_TEMPERATURE, CONF_HOST, CONF_PORT from homeassistant.core import HomeAssistant, Event from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity import DeviceInfo @@ -33,6 +33,9 @@ _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 = ['➡️', '↘️', '⬇️', '↙️', '⬅️', '↔️', '🔄'] @@ -88,6 +91,8 @@ 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 + def __init__(self, aircon: AirCon): _log('create aircon:') _log(str(aircon.__dict__)) From d4fecf036513c211e9c90b234f54db6586e5964c Mon Sep 17 00:00:00 2001 From: Necroneco Date: Sun, 19 May 2024 13:59:37 +0800 Subject: [PATCH 3/4] format code --- custom_components/ds_air/config_flow.py | 183 ++++++++++++------------ 1 file changed, 92 insertions(+), 91 deletions(-) diff --git a/custom_components/ds_air/config_flow.py b/custom_components/ds_air/config_flow.py index 2b37261..0afb279 100644 --- a/custom_components/ds_air/config_flow.py +++ b/custom_components/ds_air/config_flow.py @@ -7,17 +7,17 @@ from homeassistant import config_entries from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PORT, CONF_SCAN_INTERVAL, CONF_SENSORS -from homeassistant.core import callback, HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import FlowResult -from .const import DOMAIN, CONF_GW, DEFAULT_GW, DEFAULT_PORT, GW_LIST, DEFAULT_HOST +from .const import CONF_GW, DEFAULT_GW, DEFAULT_HOST, DEFAULT_PORT, DOMAIN, GW_LIST from .ds_air_service.service import Service from .hass_inst import GetHass _LOGGER = logging.getLogger(__name__) -def _log(s: str) -> object: +def _log(s: str) -> None: s = str(s) for i in s.split("\n"): _LOGGER.debug(i) @@ -33,10 +33,7 @@ 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") @@ -44,45 +41,48 @@ async def async_step_user( 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: - return self.async_create_entry( - title="金制空气", data=self.user_input - ) + 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 + 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(CONF_HOST, default=DEFAULT_HOST): str, - vol.Required(CONF_PORT, default=DEFAULT_PORT): int, - vol.Required(CONF_GW, default=DEFAULT_GW): vol.In(GW_LIST), - vol.Required(CONF_SCAN_INTERVAL, default=5): int, - vol.Required(CONF_SENSORS, default=True): bool - }), errors=errors + data_schema=vol.Schema( + { + vol.Required(CONF_HOST, default=DEFAULT_HOST): str, + vol.Required(CONF_PORT, default=DEFAULT_PORT): int, + vol.Required(CONF_GW, default=DEFAULT_GW): vol.In(GW_LIST), + vol.Required(CONF_SCAN_INTERVAL, default=5): int, + vol.Required(CONF_SENSORS, default=True): bool, + } + ), + errors=errors, ) @staticmethod @callback - def async_get_options_flow( - config_entry: ConfigEntry, - ) -> DsAirOptionsFlowHandler: + def async_get_options_flow(config_entry: ConfigEntry) -> DsAirOptionsFlowHandler: """Options callback for DS-AIR.""" return DsAirOptionsFlowHandler(config_entry) + class DsAirOptionsFlowHandler(config_entries.OptionsFlow): - """Config flow options for intergration""" - + """Config flow options for integration""" + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: """Initialize options flow.""" self.config_entry = config_entry @@ -90,10 +90,18 @@ def __init__(self, config_entry: config_entries.ConfigEntry) -> None: 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 = list(map(lambda state: state.entity_id, - filter(lambda state: state.attributes.get("device_class") == "temperature", sensors))) - self._sensors_humi = list(map(lambda state: state.entity_id, - filter(lambda state: state.attributes.get("device_class") == "humidity", sensors))) + self._sensors_temp = list( + map( + lambda state: state.entity_id, + filter(lambda state: state.attributes.get("device_class") == "temperature", sensors), + ) + ) + self._sensors_humi = list( + map( + lambda state: state.entity_id, + filter(lambda state: state.attributes.get("device_class") == "humidity", sensors), + ) + ) self._len = len(self._climates) self._cur = -1 self.host = CONF_HOST @@ -101,74 +109,72 @@ def __init__(self, config_entry: config_entries.ConfigEntry) -> None: 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: + + 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: + menu_options=["adjust_config", "bind_sensors"], + ) + 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 + 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) - return self.async_create_entry(title='', data={}) + return self.async_create_entry(title="", data={}) else: - self.user_input['_invaild'] = True + 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 + 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_schema=vol.Schema( + { + vol.Required(CONF_HOST, default=self.config_entry.data[CONF_HOST]): str, + vol.Required(CONF_PORT, default=self.config_entry.data[CONF_PORT]): int, + vol.Required(CONF_GW, default=self.config_entry.data[CONF_GW]): vol.In(GW_LIST), + vol.Required(CONF_SCAN_INTERVAL, default=self.config_entry.data[CONF_SCAN_INTERVAL]): int, + vol.Required(CONF_SENSORS, default=False): bool, + } + ), + errors=errors, ) - - async def async_step_bind_sensors( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + + 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) if user_input is not None: - self._config_data.append({ - "climate": user_input.get("climate"), - "sensor_temp": user_input.get("sensor_temp"), - "sensor_humi": user_input.get("sensor_humi") - }) + self._config_data.append( + { + "climate": user_input.get("climate"), + "sensor_temp": user_input.get("sensor_temp"), + "sensor_humi": user_input.get("sensor_humi"), + } + ) self._cur = self._cur + 1 if self._cur > (self._len - 1): return self.async_create_entry(title="", data={"link": self._config_data}) @@ -176,18 +182,13 @@ async def async_step_bind_sensors( step_id="bind_sensors", data_schema=vol.Schema( { - vol.Required( - "climate", - default=self._climates[self._cur] - ): vol.In([self._climates[self._cur]]), + vol.Required("climate", default=self._climates[self._cur]): vol.In([self._climates[self._cur]]), vol.Optional("sensor_temp"): vol.In(self._sensors_temp), - vol.Optional("sensor_humi"): vol.In(self._sensors_humi) + vol.Optional("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) From bc0032ce25455176168964a578a10c026bfd5c13 Mon Sep 17 00:00:00 2001 From: Necroneco Date: Sun, 19 May 2024 14:04:46 +0800 Subject: [PATCH 4/4] Show friendly name when bind sensors --- custom_components/ds_air/config_flow.py | 43 ++++++++++++++++--------- 1 file changed, 27 insertions(+), 16 deletions(-) diff --git a/custom_components/ds_air/config_flow.py b/custom_components/ds_air/config_flow.py index 0afb279..b2e37f4 100644 --- a/custom_components/ds_air/config_flow.py +++ b/custom_components/ds_air/config_flow.py @@ -5,8 +5,16 @@ import voluptuous as vol from homeassistant import config_entries +from homeassistant.components.sensor import SensorDeviceClass from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST, CONF_PORT, CONF_SCAN_INTERVAL, CONF_SENSORS +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_FRIENDLY_NAME, + CONF_HOST, + CONF_PORT, + CONF_SCAN_INTERVAL, + CONF_SENSORS, +) from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import FlowResult @@ -90,18 +98,16 @@ def __init__(self, config_entry: config_entries.ConfigEntry) -> None: 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 = list( - map( - lambda state: state.entity_id, - filter(lambda state: state.attributes.get("device_class") == "temperature", sensors), - ) - ) - self._sensors_humi = list( - map( - lambda state: state.entity_id, - filter(lambda state: state.attributes.get("device_class") == "humidity", sensors), - ) - ) + self._sensors_temp = { + 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 + } + self._sensors_humi = { + state.entity_id: f"{state.attributes.get(ATTR_FRIENDLY_NAME, state.entity_id)} ({state.entity_id})" + for state in sensors + if state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.HUMIDITY + } self._len = len(self._climates) self._cur = -1 self.host = CONF_HOST @@ -178,13 +184,18 @@ async def async_step_bind_sensors(self, user_input: dict[str, Any] | None = None self._cur = self._cur + 1 if self._cur > (self._len - 1): 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) + 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( step_id="bind_sensors", data_schema=vol.Schema( { - vol.Required("climate", default=self._climates[self._cur]): vol.In([self._climates[self._cur]]), - vol.Optional("sensor_temp"): vol.In(self._sensors_temp), - vol.Optional("sensor_humi"): vol.In(self._sensors_humi), + 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), } ), )