From ea15d489a0bedb92e5cbc018909882fd409aa150 Mon Sep 17 00:00:00 2001 From: xrh0905 <1014930533@qq.com> Date: Sat, 11 Feb 2023 11:38:36 +0800 Subject: [PATCH 01/12] Check NoneType when merge list. --- custom_components/ds_air/ds_air_service/service.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/custom_components/ds_air/ds_air_service/service.py b/custom_components/ds_air/ds_air_service/service.py index 8778d68..fa38198 100644 --- a/custom_components/ds_air/ds_air_service/service.py +++ b/custom_components/ds_air/ds_air_service/service.py @@ -202,7 +202,14 @@ def destroy(): @staticmethod def get_aircons(): - return Service._new_aircons+Service._aircons+Service._bathrooms + 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 + return aircons @staticmethod def control(aircon: AirCon, status: AirConStatus): From 3ba793e0e9dc3b68cb5eae4be031ad92546baff1 Mon Sep 17 00:00:00 2001 From: Necroneco Date: Wed, 1 Mar 2023 21:19:10 +0800 Subject: [PATCH 02/12] Defer updating linked TH sensor data --- custom_components/ds_air/climate.py | 68 ++++++++++++----------------- 1 file changed, 29 insertions(+), 39 deletions(-) diff --git a/custom_components/ds_air/climate.py b/custom_components/ds_air/climate.py index 6bed719..c554093 100644 --- a/custom_components/ds_air/climate.py +++ b/custom_components/ds_air/climate.py @@ -61,51 +61,31 @@ async def async_setup_entry( climates.append(DsAir(aircon)) async_add_entities(climates) link = entry.options.get("link") - sensor_temp_map = {} - sensor_humi_map = {} + sensor_temp_map: dict[str, list[DsAir]] = {} + sensor_humi_map: dict[str, list[DsAir]] = {} if link is not None: for i in link: - if i.get("sensor_temp") is not None: - climate = None - for j in climates: - if i.get("climate") == j.name: - climate = j - break - if sensor_temp_map.get(i.get("sensor_temp")) is not None: - sensor_temp_map[i.get("sensor_temp")].append(climate) - else: - sensor_temp_map[i.get("sensor_temp")] = [climate] - if i.get("sensor_humi") is not None: - climate = None - for j in climates: - if i.get("climate") == j.name: - climate = j - break - if sensor_humi_map.get(i.get("sensor_humi")) is not None: - sensor_humi_map[i.get("sensor_humi")].append(climate) - else: - sensor_humi_map[i.get("sensor_humi")] = [climate] - - async def listner(event: Event): - if event.data.get("entity_id") in sensor_temp_map: - for climate in sensor_temp_map[event.data.get("entity_id")]: + climate_name = i.get("climate") + if climate := next(c for c in climates if c.name == 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 + if humi_entity_id := i.get("sensor_humi"): + sensor_humi_map.setdefault(humi_entity_id, []).append(climate) + climate.linked_humi_entity_id = humi_entity_id + + async def listener(event: Event): + sensor_id = event.data.get("entity_id") + if sensor_id in sensor_temp_map: + for climate in sensor_temp_map[sensor_id]: climate.update_cur_temp(event.data.get("new_state").state) - elif event.data.get("entity_id") in sensor_humi_map: - for climate in sensor_humi_map[event.data.get("entity_id")]: + elif sensor_id in sensor_humi_map: + 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()), listner) + 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 - for entity_id in sensor_temp_map.keys(): - state = hass.states.get(entity_id) - if state is not None: - for climate in sensor_temp_map[entity_id]: - climate.update_cur_temp(state.state) - for entity_id in sensor_humi_map.keys(): - state = hass.states.get(entity_id) - if state is not None: - for climate in sensor_humi_map[entity_id]: - climate.update_cur_humi(state.state) + class DsAir(ClimateEntity): """Representation of a demo climate device.""" @@ -118,6 +98,8 @@ def __init__(self, aircon: AirCon): self._name = aircon.alias self._device_info = aircon self._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 @@ -125,6 +107,14 @@ def __init__(self, aircon: AirCon): from .ds_air_service.service import Service Service.register_status_hook(aircon, self._status_change_hook) + 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): + self.update_cur_temp(state.state) + if self.linked_humi_entity_id: + if state := self.hass.states.get(self.linked_humi_entity_id): + self.update_cur_humi(state.state) + def _status_change_hook(self, **kwargs): _log('hook:') if kwargs.get('aircon') is not None: From d34753a96514310d32f654fcd9534b5c96bc80ef Mon Sep 17 00:00:00 2001 From: xrh0905 <1014930533@qq.com> Date: Fri, 30 Jun 2023 23:50:37 +0800 Subject: [PATCH 03/12] Small improvements Add author info Update VOC Device Class Support Temperature Control at 0.5 Remove air quality Update AUTO Mode Logic Fix AC & Sensors Device info Allow fallback to AUTO or RELAX or COLD when exit SLEEP Try to refactor config flow --- custom_components/ds_air/air_quality.py | 55 --------- custom_components/ds_air/climate.py | 111 ++++++++++++------ custom_components/ds_air/config_flow.py | 101 ++++++++++++---- custom_components/ds_air/const.py | 16 +-- .../ds_air/ds_air_service/ctrl_enum.py | 5 +- custom_components/ds_air/manifest.json | 2 +- custom_components/ds_air/sensor.py | 2 +- custom_components/ds_air/strings.json | 74 ++++++++---- custom_components/ds_air/translations/en.json | 35 +++++- .../ds_air/translations/zh-Hans.json | 31 ++++- 10 files changed, 280 insertions(+), 152 deletions(-) delete mode 100644 custom_components/ds_air/air_quality.py diff --git a/custom_components/ds_air/air_quality.py b/custom_components/ds_air/air_quality.py deleted file mode 100644 index 656e222..0000000 --- a/custom_components/ds_air/air_quality.py +++ /dev/null @@ -1,55 +0,0 @@ -"""Demo platform that offers fake air quality data.""" -from homeassistant.components.air_quality import AirQualityEntity - - -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the Air Quality.""" - async_add_entities( - [DemoAirQuality("Home", 14, 23, 100), DemoAirQuality("Office", 4, 16, None)] - ) - - -async def async_setup_entry(hass, config_entry, async_add_entities): - """Set up the Demo config entry.""" - await async_setup_platform(hass, {}, async_add_entities) - - -class DemoAirQuality(AirQualityEntity): - """Representation of Air Quality data.""" - - def __init__(self, name, pm_2_5, pm_10, n2o): - """Initialize the Demo Air Quality.""" - self._name = name - self._pm_2_5 = pm_2_5 - self._pm_10 = pm_10 - self._n2o = n2o - - @property - def name(self): - """Return the name of the sensor.""" - return f"Demo Air Quality {self._name}" - - @property - def should_poll(self): - """No polling needed for Demo Air Quality.""" - return False - - @property - def particulate_matter_2_5(self): - """Return the particulate matter 2.5 level.""" - return self._pm_2_5 - - @property - def particulate_matter_10(self): - """Return the particulate matter 10 level.""" - return self._pm_10 - - @property - def nitrogen_oxide(self): - """Return the nitrogen oxide (N2O) level.""" - return self._n2o - - @property - def attribution(self): - """Return the attribution.""" - return "Powered by Home Assistant" diff --git a/custom_components/ds_air/climate.py b/custom_components/ds_air/climate.py index c554093..5f808a3 100644 --- a/custom_components/ds_air/climate.py +++ b/custom_components/ds_air/climate.py @@ -1,5 +1,5 @@ """ -Demo platform that offers a fake climate device. +Daikin platform that offers climate devices. For more details about this platform, please refer to the documentation https://home-assistant.io/components/demo/ @@ -11,13 +11,22 @@ 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, +""" 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, + 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) + FAN_AUTO, FAN_LOW, FAN_MEDIUM, FAN_HIGH) """ +from homeassistant.components.climate import ( + ClimateEntity, + ClimateEntityFeature, + HVACMode, + PRESET_NONE, PRESET_SLEEP, + 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.core import HomeAssistant, Event @@ -32,10 +41,9 @@ from .ds_air_service.dao import AirCon, AirConStatus from .ds_air_service.display import display -SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE | SUPPORT_SWING_MODE \ - | SUPPORT_SWING_MODE | SUPPORT_TARGET_HUMIDITY -#FAN_LIST = ['最弱', '稍弱', '中等', '稍强', '最强', '自动'] -FAN_LIST = [FAN_LOW, '稍弱', FAN_MEDIUM, '稍强', FAN_HIGH, FAN_AUTO] +_SUPPORT_FLAGS = ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE | ClimateEntityFeature.PRESET_MODE +# | ClimateEntityFeature.SWING_MODE | ClimateEntityFeature.TARGET_HUMIDITY +FAN_LIST = [ FAN_LOW, '稍弱', FAN_MEDIUM, '稍强', FAN_HIGH, FAN_AUTO] SWING_LIST = ['➡️', '↘️', '⬇️', '↙️', '⬅️', '↔️', '🔄'] PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ @@ -88,7 +96,7 @@ async def listener(event: Event): class DsAir(ClimateEntity): - """Representation of a demo climate device.""" + """Representation of a Daikin climate device.""" def __init__(self, aircon: AirCon): _log('create aircon:') @@ -195,7 +203,7 @@ def hvac_mode(self) -> str: Need to be one of HVAC_MODE_*. """ if self._device_info.status.switch == EnumControl.Switch.OFF: - return HVAC_MODE_OFF + return HVACMode.OFF else: return EnumControl.get_mode_name(self._device_info.status.mode.value) @@ -205,18 +213,16 @@ def hvac_modes(self): li = [] aircon = self._device_info if aircon.cool_mode: - li.append(HVAC_MODE_COOL) + li.append(HVACMode.COOL) if aircon.heat_mode or aircon.pre_heat_mode: - li.append(HVAC_MODE_HEAT) + li.append(HVACMode.HEAT) if aircon.auto_dry_mode or aircon.dry_mode or aircon.more_dry_mode: - li.append(HVAC_MODE_DRY) + li.append(HVACMode.DRY) if aircon.ventilation_mode: - li.append(HVAC_MODE_FAN_ONLY) - if aircon.relax_mode or aircon.auto_mode: - li.append(HVAC_MODE_AUTO) - if aircon.sleep_mode: - li.append(HVAC_MODE_HEAT_COOL) - li.append(HVAC_MODE_OFF) + li.append(HVACMode.FAN_ONLY) + if aircon.relax_mode or aircon.sleep_mode or aircon.auto_mode: + li.append(HVACMode.AUTO) + li.append(HVACMode.OFF) return li @property @@ -238,7 +244,7 @@ def target_temperature(self): @property def target_temperature_step(self): """Return the supported step of target temperature.""" - return 1 + return 0.5 @property def target_temperature_high(self): @@ -264,7 +270,10 @@ def preset_mode(self) -> Optional[str]: Requires SUPPORT_PRESET_MODE. """ - return None + if self._device_info.status.mode == EnumControl.Mode.SLEEP: + return PRESET_SLEEP + else: + return PRESET_NONE @property def preset_modes(self) -> Optional[List[str]]: @@ -272,7 +281,12 @@ def preset_modes(self) -> Optional[List[str]]: Requires SUPPORT_PRESET_MODE. """ - return None + result = [] + aircon = self._device_info + if aircon.sleep_mode: + result.append(PRESET_SLEEP) + result.append(PRESET_NONE) + return result @property def is_aux_heat(self): @@ -312,8 +326,8 @@ def set_temperature(self, **kwargs): 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 - new_status.setted_temp = round(kwargs.get(ATTR_TEMPERATURE)) * 10 + status.setted_temp = round(kwargs.get(ATTR_TEMPERATURE) * 10.0) + new_status.setted_temp = round(kwargs.get(ATTR_TEMPERATURE) * 10.0) from .ds_air_service.service import Service Service.control(self._device_info, new_status) self.schedule_update_ha_state() @@ -347,7 +361,7 @@ def set_hvac_mode(self, hvac_mode: str) -> None: aircon = self._device_info status = aircon.status new_status = AirConStatus() - if hvac_mode == HVAC_MODE_OFF: + if hvac_mode == HVACMode.OFF: status.switch = EnumControl.Switch.OFF new_status.switch = EnumControl.Switch.OFF from .ds_air_service.service import Service @@ -357,29 +371,29 @@ def set_hvac_mode(self, hvac_mode: str) -> None: new_status.switch = EnumControl.Switch.ON m = EnumControl.Mode mode = None - if hvac_mode == HVAC_MODE_COOL: + if hvac_mode == HVACMode.COOL: mode = m.COLD - elif hvac_mode == HVAC_MODE_HEAT: + elif hvac_mode == HVACMode.HEAT: if aircon.heat_mode: mode = m.HEAT else: mode = m.PREHEAT - elif hvac_mode == HVAC_MODE_DRY: + elif hvac_mode == HVACMode.DRY: if aircon.auto_dry_mode: mode = m.AUTODRY elif aircon.more_dry_mode: mode = m.MOREDRY else: mode = m.DRY - elif hvac_mode == HVAC_MODE_FAN_ONLY: + elif hvac_mode == HVACMode.FAN_ONLY: mode = m.VENTILATION - elif hvac_mode == HVAC_MODE_AUTO: + elif hvac_mode == HVACMode.AUTO: if aircon.auto_mode: mode = m.AUTO - else: + elif aircon.relax_mode: mode = m.RELAX - elif hvac_mode == HVAC_MODE_HEAT_COOL: - mode = m.SLEEP + else: + mode = m.SLEEP status.mode = mode new_status.mode = mode from .ds_air_service.service import Service @@ -400,7 +414,26 @@ def set_swing_mode(self, swing_mode): self.schedule_update_ha_state() def set_preset_mode(self, preset_mode: str) -> None: - pass + aircon = self._device_info + status = aircon.status + new_status = AirConStatus() + m = EnumControl.Mode + mode = None + if preset_mode == PRESET_NONE: + if aircon.auto_mode: + mode = m.AUTO + elif aircon.relax_mode: + mode = m.RELAX + else: + mode = m.COLD + else: + if preset_mode == PRESET_SLEEP: + mode = m.SLEEP + status.mode = mode + new_status.mode = mode + from .ds_air_service.service import Service + Service.control(self._device_info, new_status) + self.schedule_update_ha_state() def turn_aux_heat_on(self) -> None: pass @@ -411,6 +444,12 @@ def turn_aux_heat_off(self) -> None: @property def supported_features(self) -> int: """Return the list of supported features.""" + SUPPORT_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.relax_mode: + SUPPORT_FLAGS = SUPPORT_FLAGS | ClimateEntityFeature.TARGET_HUMIDITY return SUPPORT_FLAGS @property @@ -436,7 +475,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 4debb10..2b37261 100644 --- a/custom_components/ds_air/config_flow.py +++ b/custom_components/ds_air/config_flow.py @@ -80,15 +80,13 @@ def async_get_options_flow( """Options callback for DS-AIR.""" return DsAirOptionsFlowHandler(config_entry) - class DsAirOptionsFlowHandler(config_entries.OptionsFlow): - """Config flow options for sensors binding.""" - - def __init__(self, entry: ConfigEntry) -> None: - """Initialize DSAir options flow.""" - self.config_entry = entry - self._len = 3 - self._cur = 0 + """Config flow options for intergration""" + + def __init__(self, config_entry: config_entries.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") @@ -96,31 +94,86 @@ def __init__(self, entry: ConfigEntry) -> None: 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._config_data = [] - + 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 + self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Manage the options.""" - self._len = len(self._climates) - self._cur = 0 - return await self.async_step_user() + 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_user( + 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) + return self.async_create_entry(title='', data={}) + else: + self.user_input['_invaild'] = True + if CONF_SENSORS: + return self.async_show_form( + step_id="adjust_config", + data_schema=vol.Schema({ + vol.Required(CONF_HOST, default=self.config_entry.data[CONF_HOST]): str, + vol.Required(CONF_PORT, default=self.config_entry.data[CONF_PORT]): int, + vol.Required(CONF_GW, default=self.config_entry.data[CONF_GW]): vol.In(GW_LIST), + vol.Required(CONF_SCAN_INTERVAL, default=self.config_entry.data[CONF_SCAN_INTERVAL]): int, + vol.Required(CONF_SENSORS, default=True): bool, + vol.Required("temp", default=self.config_entry.data["temp"]): bool, + vol.Required("humidity", default=self.config_entry.data["humidity"]): bool, + vol.Required("pm25", default=self.config_entry.data["pm25"]): bool, + vol.Required("co2", default=self.config_entry.data["co2"]): bool, + vol.Required("tvoc", default=self.config_entry.data["tvoc"]): bool, + vol.Required("voc", default=self.config_entry.data["voc"]): bool, + vol.Required("hcho", default=self.config_entry.data["hcho"]): bool, + }), errors=errors + ) + else: + return self.async_show_form( + step_id="adjust_config", + data_schema=vol.Schema({ + vol.Required(CONF_HOST, default=self.config_entry.data[CONF_HOST]): str, + vol.Required(CONF_PORT, default=self.config_entry.data[CONF_PORT]): int, + vol.Required(CONF_GW, default=self.config_entry.data[CONF_GW]): vol.In(GW_LIST), + vol.Required(CONF_SCAN_INTERVAL, default=self.config_entry.data[CONF_SCAN_INTERVAL]): int, + vol.Required(CONF_SENSORS, default=False): bool + }), errors=errors + ) + + async def async_step_bind_sensors( self, user_input: dict[str, Any] | None = None ) -> FlowResult: - """Handle a flow initialized by the user.""" + """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") }) - if self._cur == self._len: + self._cur = self._cur + 1 + if self._cur > (self._len - 1): return self.async_create_entry(title="", data={"link": self._config_data}) - - form = self.async_show_form( - step_id="user", + return self.async_show_form( + step_id="bind_sensors", data_schema=vol.Schema( { vol.Required( @@ -133,6 +186,8 @@ async def async_step_user( ) ) - self._cur = self._cur + 1 - - return form + async def async_step_empty( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """No AC found.""" + return await self.async_step_init(user_input) diff --git a/custom_components/ds_air/const.py b/custom_components/ds_air/const.py index 8a4be4c..600eb71 100644 --- a/custom_components/ds_air/const.py +++ b/custom_components/ds_air/const.py @@ -1,6 +1,6 @@ from homeassistant.const import TEMP_CELSIUS, PERCENTAGE, CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, \ - CONCENTRATION_PARTS_PER_MILLION, CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER, DEVICE_CLASS_HUMIDITY, \ - DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_CO2, DEVICE_CLASS_PM25 + CONCENTRATION_PARTS_PER_MILLION, CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER +from homeassistant.components.sensor import SensorDeviceClass from .ds_air_service.ctrl_enum import EnumSensor @@ -11,11 +11,11 @@ DEFAULT_GW = "DTA117C611" GW_LIST = ["DTA117C611", "DTA117B611"] SENSOR_TYPES = { - "temp": [TEMP_CELSIUS, None, DEVICE_CLASS_TEMPERATURE, 10], - "humidity": [PERCENTAGE, None, DEVICE_CLASS_HUMIDITY, 10], - "pm25": [CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, None, DEVICE_CLASS_PM25, 1], - "co2": [CONCENTRATION_PARTS_PER_MILLION, None, DEVICE_CLASS_CO2, 1], - "tvoc": [CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER, None, None, 100], - "voc": [None, None, None, EnumSensor.Voc], + "temp": [TEMP_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_MILLIGRAMS_PER_CUBIC_METER, None, SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS_PARTS, 100], + "voc": [None, None, SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS_PARTS, EnumSensor.Voc], "hcho": [CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER, None, None, 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 7e66f61..a919c5a 100644 --- a/custom_components/ds_air/ds_air_service/ctrl_enum.py +++ b/custom_components/ds_air/ds_air_service/ctrl_enum.py @@ -292,9 +292,10 @@ class Mode(IntEnum): MOREDRY = 9 +#_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 = [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] - + HVAC_MODE_DRY, HVAC_MODE_AUTO, HVAC_MODE_AUTO, HVAC_MODE_HEAT, HVAC_MODE_DRY] class Switch(IntEnum): OFF = 0 diff --git a/custom_components/ds_air/manifest.json b/custom_components/ds_air/manifest.json index c4c630e..89dd009 100644 --- a/custom_components/ds_air/manifest.json +++ b/custom_components/ds_air/manifest.json @@ -3,7 +3,7 @@ "name": "DS-AIR", "documentation": "https://github.com/mypal/ha-dsair", "dependencies": [], - "codeowners": [], + "codeowners": ["@mypal"], "requirements": [], "version": "1.3.3", "config_flow": true diff --git a/custom_components/ds_air/sensor.py b/custom_components/ds_air/sensor.py index fe96bdb..d59ed49 100644 --- a/custom_components/ds_air/sensor.py +++ b/custom_components/ds_air/sensor.py @@ -45,7 +45,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/strings.json b/custom_components/ds_air/strings.json index 5d3f8c3..2f4b115 100644 --- a/custom_components/ds_air/strings.json +++ b/custom_components/ds_air/strings.json @@ -2,41 +2,71 @@ "config": { "step": { "user": { - "title": "\u91d1\u5236\u7a7a\u6c14", - "description": "\u672c\u63d2\u4ef6\u652f\u6301\u5927\u91d1DTA117B611/DTA117C611\u4e24\u6b3e\u7a7a\u8c03\u7f51\u5173", + "title": "DS-AIR", + "description": "Support DTA117B611/DTA117C611", "data": { - "host": "host", - "port": "port", - "gw": "\u7f51\u5173\u578b\u53f7", - "scan_interval": "\u4f20\u611f\u5668\u66f4\u65b0\u9891\u7387\uff08\u5355\u4f4d\uff1a\u5206\u949f\uff09", - "sensors": "\u662f\u5426\u6709\u4f20\u611f\u5668", - "temp": "\u521b\u5efatemperature\u5b9e\u4f53", - "humidity": "\u521b\u5efahumidity\u5b9e\u4f53", - "pm25": "\u521b\u5efapm25\u5b9e\u4f53", - "co2": "\u521b\u5efaco2\u5b9e\u4f53", - "tvoc": "\u521b\u5efatvoc\u5b9e\u4f53", - "voc": "\u521b\u5efavoc\u5b9e\u4f53", - "hcho": "\u521b\u5efahcho\u5b9e\u4f53" + "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": "\u53ea\u5141\u8bb8\u521b\u5efa\u4e00\u4e2a" + "single_instance_allowed": "Only one instance is allowed." }, - "flow_title": "\u91d1\u5236\u7a7a\u6c14" + "flow_title": "DS-AIR" }, "options": { "step": { - "user": { - "title": "\u6e29\u5ea6\u4f20\u611f\u5668\u5173\u8054", - "description": "\u53ef\u4ee5\u4e3a\u7a7a\u8c03\u5173\u8054\u6e29\u5ea6\u4f20\u611f\u5668", + "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": "sensor entity_id" + "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/custom_components/ds_air/translations/en.json b/custom_components/ds_air/translations/en.json index 097ef2d..2f4b115 100644 --- a/custom_components/ds_air/translations/en.json +++ b/custom_components/ds_air/translations/en.json @@ -29,13 +29,42 @@ }, "options": { "step": { - "user": { - "title": "Binding sensors", - "description": "Bind sensors to climate", + "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" } } } diff --git a/custom_components/ds_air/translations/zh-Hans.json b/custom_components/ds_air/translations/zh-Hans.json index 6bb0206..96c2a03 100644 --- a/custom_components/ds_air/translations/zh-Hans.json +++ b/custom_components/ds_air/translations/zh-Hans.json @@ -29,13 +29,42 @@ }, "options": { "step": { - "user": { + "init": { + "title": "金制空气", + "menu_options": { + "adjust_config": "调整设置", + "bind_sensors": "关联传感器" + } + }, + "adjust_config": { + "title": "修改设置", + "description": "", + "data": { + "host": "网关IP", + "port": "网关端口", + "gw": "网关型号", + "scan_interval": "传感器更新频率(单位:分钟)", + "sensors": "是否有传感器", + "temp": "创建温度实体", + "humidity": "创建湿度实体", + "pm25": "创建PM2.5实体", + "co2": "创建CO2实体", + "tvoc": "创建tvoc实体", + "voc": "创建voc实体", + "hcho": "创建hcho实体" + } + }, + "bind_sensors": { "title": "传感器关联", "description": "为空调关联温湿度传感器", "data": { "climate": "空调名称", "sensor_temp": "温度传感器实体ID", "sensor_humi": "湿度传感器实体ID" + }, + "empty": { + "title": "无数据", + "description": "没有可操作的空调" } } } From b26d700613d4a98a488994da29cc90ac6dcb9022 Mon Sep 17 00:00:00 2001 From: xrh0905 <1014930533@qq.com> Date: Sat, 1 Jul 2023 09:35:05 +0800 Subject: [PATCH 04/12] Make RELAX matchs Preset_Comfort --- custom_components/ds_air/climate.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/custom_components/ds_air/climate.py b/custom_components/ds_air/climate.py index 5f808a3..cd0013a 100644 --- a/custom_components/ds_air/climate.py +++ b/custom_components/ds_air/climate.py @@ -24,7 +24,7 @@ ClimateEntity, ClimateEntityFeature, HVACMode, - PRESET_NONE, PRESET_SLEEP, + PRESET_NONE, PRESET_SLEEP, PRESET_COMFORT, FAN_AUTO, FAN_LOW, FAN_MEDIUM, FAN_HIGH ) from homeassistant.config_entries import ConfigEntry @@ -272,6 +272,8 @@ def preset_mode(self) -> Optional[str]: """ if self._device_info.status.mode == EnumControl.Mode.SLEEP: return PRESET_SLEEP + elif self._device_info.status.mode == EnumControl.Mode.RELAX: + return PRESET_COMFORT else: return PRESET_NONE @@ -285,6 +287,8 @@ def preset_modes(self) -> Optional[List[str]]: aircon = self._device_info if aircon.sleep_mode: result.append(PRESET_SLEEP) + if aircon.relax_mode: + result.append(PRESET_COMFORT) result.append(PRESET_NONE) return result @@ -429,6 +433,8 @@ def set_preset_mode(self, preset_mode: str) -> None: else: if preset_mode == PRESET_SLEEP: mode = m.SLEEP + elif preset_mode == PRESET_COMFORT: + mode = m.RELAX status.mode = mode new_status.mode = mode from .ds_air_service.service import Service From 7207cbd6c7be4a44b1781dba5286f99524ba63af Mon Sep 17 00:00:00 2001 From: xrh0905 <1014930533@qq.com> Date: Sun, 16 Jul 2023 16:04:39 +0800 Subject: [PATCH 05/12] Add support for HVAC Actions. --- custom_components/ds_air/climate.py | 7 +++++-- custom_components/ds_air/ds_air_service/ctrl_enum.py | 11 +++++++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/custom_components/ds_air/climate.py b/custom_components/ds_air/climate.py index cd0013a..382c273 100644 --- a/custom_components/ds_air/climate.py +++ b/custom_components/ds_air/climate.py @@ -23,7 +23,7 @@ from homeassistant.components.climate import ( ClimateEntity, ClimateEntityFeature, - HVACMode, + HVACMode, HVACAction, PRESET_NONE, PRESET_SLEEP, PRESET_COMFORT, FAN_AUTO, FAN_LOW, FAN_MEDIUM, FAN_HIGH ) @@ -194,7 +194,10 @@ def target_humidity(self): @property def hvac_action(self): """Return current operation ie. heat, cool, idle.""" - return None + if self._device_info.status.switch == EnumControl.Switch.OFF: + return HVACAction.OFF + else: + return EnumControl.get_action_name(self._device_info.status.mode.value) @property def hvac_mode(self) -> str: 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 a919c5a..a0212d4 100644 --- a/custom_components/ds_air/ds_air_service/ctrl_enum.py +++ b/custom_components/ds_air/ds_air_service/ctrl_enum.py @@ -2,7 +2,8 @@ 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 + FAN_AUTO, FAN_LOW, FAN_MEDIUM, FAN_HIGH, \ + HVACAction class EnumCmdType(IntEnum): @@ -291,11 +292,14 @@ class Mode(IntEnum): PREHEAT = 8 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 = [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_ACTION_LIST = [HVACAction.COOLING, HVACAction.DRYING, HVACAction.FAN, None, HVACAction.HEATING, + HVACAction.DRYING, None, None, HVACAction.PREHEATING, HVACAction.DRYING] class Switch(IntEnum): OFF = 0 @@ -329,6 +333,9 @@ class EnumControl: @staticmethod def get_mode_name(idx): return _MODE_NAME_LIST[idx] + + def get_action_name(idx): + return _MODE_ACTION_LIST[idx] @staticmethod def get_mode_enum(name): From 097ef3f1e1b26f1d0912c2468d53a18f3886a4c5 Mon Sep 17 00:00:00 2001 From: Necroneco Date: Mon, 21 Aug 2023 12:44:49 +0800 Subject: [PATCH 06/12] Add hacs.json --- hacs.json | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 hacs.json diff --git a/hacs.json b/hacs.json new file mode 100644 index 0000000..60d4bff --- /dev/null +++ b/hacs.json @@ -0,0 +1,5 @@ +{ + "name": "DS-AIR", + "render_readme": true, + "homeassistant": "2023.7.0" +} \ No newline at end of file From 1ed3dc3e2c6e32e385463f2b44d4bec15a45085f Mon Sep 17 00:00:00 2001 From: Necroneco Date: Mon, 21 Aug 2023 12:48:20 +0800 Subject: [PATCH 07/12] =?UTF-8?q?Change=20unit=20of=20tvoc=20to=20=C2=B5g/?= =?UTF-8?q?m=C2=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- custom_components/ds_air/const.py | 2 +- custom_components/ds_air/sensor.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/custom_components/ds_air/const.py b/custom_components/ds_air/const.py index 600eb71..8ca56f8 100644 --- a/custom_components/ds_air/const.py +++ b/custom_components/ds_air/const.py @@ -15,7 +15,7 @@ "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_MILLIGRAMS_PER_CUBIC_METER, None, SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS_PARTS, 100], + "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], } diff --git a/custom_components/ds_air/sensor.py b/custom_components/ds_air/sensor.py index d59ed49..d211b98 100644 --- a/custom_components/ds_air/sensor.py +++ b/custom_components/ds_air/sensor.py @@ -95,10 +95,11 @@ def parse_data(self, device: Sensor, not_update: bool = False): """Parse data sent by gateway.""" self._is_available = device.connected if UNINITIALIZED_VALUE != getattr(device, self._data_key): - if type(SENSOR_TYPES.get(self._data_key)[3]) != int: + 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) / SENSOR_TYPES.get(self._data_key)[3] + self._state = getattr(device, self._data_key) / scaling if not not_update: self.schedule_update_ha_state() From 86a90a6f0e55ef19cab628ab81c9cda8a79895f1 Mon Sep 17 00:00:00 2001 From: Necroneco Date: Thu, 4 Jan 2024 11:12:44 +0800 Subject: [PATCH 08/12] 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 09/12] 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 10/12] 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 11/12] 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), } ), ) From 6b0d4c9b29e6f5149a734e5bf67657d0efc1b738 Mon Sep 17 00:00:00 2001 From: mypal Date: Wed, 26 Jun 2024 14:45:56 +0800 Subject: [PATCH 12/12] bump version to 1.3.5 --- 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 89dd009..f8ebe21 100644 --- a/custom_components/ds_air/manifest.json +++ b/custom_components/ds_air/manifest.json @@ -5,6 +5,6 @@ "dependencies": [], "codeowners": ["@mypal"], "requirements": [], - "version": "1.3.3", + "version": "1.3.5", "config_flow": true }