diff --git a/README.md b/README.md index a8376eb..ba473b9 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,10 @@ 或使用hacs载入自定义存储库,设置URL```https://github.com/mypal/ha-dsair``` ,类别 ```集成``` 2. 本集成已支持ha可视化配置,在配置-集成-添加集成中选择```DS-AIR``` ,依次填入网关IP、端口号、设备型号提交即可 +# TODO + +根据APP反解显示,网关可控制新风、地暖、HD(不知道是个啥设备)、新版空调、老版空调和浴室设备。由于我家只有新版室内机,所以目前只实现了这个。其他设备实现没写完,理论上都不能够支持。 + # 开发过程 本组件开发过程可在[blog](https://www.mypal.wang/blog/lun-yi-ci-jia-yong-kong-diao-jie-ru-hazhe-teng-jing-li/)查看 diff --git a/custom_components/ds_air/__init__.py b/custom_components/ds_air/__init__.py index 34ce21c..e9c6edc 100644 --- a/custom_components/ds_air/__init__.py +++ b/custom_components/ds_air/__init__.py @@ -9,11 +9,11 @@ from homeassistant.core import HomeAssistant from .hass_inst import GetHass -from .const import CONF_GW, DEFAULT_HOST, DEFAULT_PORT, DEFAULT_GW, DOMAIN +from .const import CONF_GW, DEFAULT_HOST, DEFAULT_PORT, C611, D611, DOMAIN from .ds_air_service.config import Config _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["climate", "sensor"] +PLATFORMS = ["climate", "sensor", "fan"] def _log(s: str): @@ -43,7 +43,8 @@ async def async_setup_entry( hass.data[DOMAIN][CONF_GW] = gw hass.data[DOMAIN][CONF_SCAN_INTERVAL] = scan_interval - Config.is_c611 = gw == DEFAULT_GW + Config.is_c611 = gw == C611 + Config.is_d611 = gw == D611 from .ds_air_service.service import Service await hass.async_add_executor_job(Service.init, host, port, scan_interval) diff --git a/custom_components/ds_air/climate.py b/custom_components/ds_air/climate.py index 2c5e9fb..15602eb 100644 --- a/custom_components/ds_air/climate.py +++ b/custom_components/ds_air/climate.py @@ -229,7 +229,7 @@ def current_temperature(self): if self._link_cur_temp: return self._cur_temp else: - if Config.is_c611: + if Config.is_c611 or Config.is_d611: return None else: return self._device_info.status.current_temp / 10 diff --git a/custom_components/ds_air/const.py b/custom_components/ds_air/const.py index 87e9218..8dd9de7 100644 --- a/custom_components/ds_air/const.py +++ b/custom_components/ds_air/const.py @@ -8,8 +8,13 @@ CONF_GW = "gw" DEFAULT_HOST = "192.168.1." DEFAULT_PORT = 8008 -DEFAULT_GW = "DTA117C611" -GW_LIST = ["DTA117C611", "DTA117B611"] + +B611 = "DTA117B611" +C611 = "DTA117C611" +D611 = "DTA117D611" +DEFAULT_GW = C611 +GW_LIST = [C611, B611, D611] + SENSOR_TYPES = { "temp": [UnitOfTemperature.CELSIUS, None, SensorDeviceClass.TEMPERATURE, 10], "humidity": [PERCENTAGE, None, SensorDeviceClass.HUMIDITY, 10], @@ -19,3 +24,10 @@ "voc": [None, None, SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS_PARTS, EnumSensor.Voc], "hcho": [CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER, None, None, 100], } + +SMALL_VAM_SENSOR_TYPES = { + "in_door_temp": [UnitOfTemperature.CELSIUS, None, SensorDeviceClass.TEMPERATURE, 10], + "out_door_temp": [UnitOfTemperature.CELSIUS, None, SensorDeviceClass.TEMPERATURE, 10], + "out_door_humidity": [PERCENTAGE, None, SensorDeviceClass.HUMIDITY, 1], + "pm25": [CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, None, SensorDeviceClass.PM25, 1], +} diff --git a/custom_components/ds_air/ds_air_service/config.py b/custom_components/ds_air/ds_air_service/config.py index e40047a..12489eb 100644 --- a/custom_components/ds_air/ds_air_service/config.py +++ b/custom_components/ds_air/ds_air_service/config.py @@ -1,3 +1,4 @@ class Config: is_new_version: bool = False is_c611: bool = True # 金制空气c611 or ds-air b611 + is_d611: bool = False # 金制空气d611 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..55b47b0 100644 --- a/custom_components/ds_air/ds_air_service/ctrl_enum.py +++ b/custom_components/ds_air/ds_air_service/ctrl_enum.py @@ -154,7 +154,7 @@ class EnumCmdType(IntEnum): HCHO_SET_INFO = 151 HCHO_GET_SENSORS = 152 SYS_ADDRESS_ALLOCATION = 218 - SMALL_VAM_QUERY_AIR_QUALITY = 52 + SMALL_VAM_QUERY_COMPOSITE_SITUATION = 52 SMALL_VAM_LINKAGE_CONTROL = 53 SMALL_VAM_LINKAGE_STATUS = 54 HUMIDIFIER_GET_ALL_DEVICES = 4 @@ -243,6 +243,7 @@ class AirFlow(IntEnum): #_AIR_FLOW_NAME_LIST = ['最弱', '稍弱', '中等', '稍强', '最强', '自动'] _AIR_FLOW_NAME_LIST = [FAN_LOW, '稍弱', FAN_MEDIUM, '稍强', FAN_HIGH, FAN_AUTO] +_VENT_AIR_FLOW_NAME_LIST = ['INVALID', '静音', '中速', '高速', '暴风'] class Breathe(IntEnum): CLOSE = 0 @@ -304,6 +305,8 @@ class Mode(IntEnum): 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_VENT_NAME_LIST_SMALL_VAM = ["内循环", "热交换", "自动", "防污染", "排异味"] +_MODE_VENT_NAME_LIST_STANDARD_VAM = ["旁通", "热交换", "自动"] class Switch(IntEnum): OFF = 0 @@ -345,6 +348,22 @@ def get_action_name(idx): def get_mode_enum(name): return Mode(_MODE_NAME_LIST.index(name)) + @staticmethod + def get_vent_mode_name_small_vam(idx): + return _MODE_VENT_NAME_LIST_SMALL_VAM[idx] + + @staticmethod + def get_vent_mode_enum_small_vam(name: str): + return Mode(_MODE_VENT_NAME_LIST_SMALL_VAM.index(name)) + + @staticmethod + def get_vent_mode_name_standard_vam(idx): + return _MODE_VENT_NAME_LIST_STANDARD_VAM[idx] + + @staticmethod + def get_vent_mode_enum_standard_vam(name: str): + return Mode(_MODE_VENT_NAME_LIST_STANDARD_VAM.index(name)) + @staticmethod def get_air_flow_name(idx): return _AIR_FLOW_NAME_LIST[idx] @@ -352,6 +371,14 @@ def get_air_flow_name(idx): @staticmethod def get_air_flow_enum(name): return AirFlow(_AIR_FLOW_NAME_LIST.index(name)) + + @staticmethod + def get_vent_air_flow_name(idx): + return _VENT_AIR_FLOW_NAME_LIST[idx] + + @staticmethod + def get_vent_air_flow_enum(name): + return AirFlow(_VENT_AIR_FLOW_NAME_LIST.index(name)) @staticmethod def get_fan_direction_name(idx): diff --git a/custom_components/ds_air/ds_air_service/dao.py b/custom_components/ds_air/ds_air_service/dao.py index 38d6350..a1aa7d5 100644 --- a/custom_components/ds_air/ds_air_service/dao.py +++ b/custom_components/ds_air/ds_air_service/dao.py @@ -77,7 +77,6 @@ def get_device_by_aircon(aircon: AirCon): else: return EnumDevice.AIRCON - class Geothermic(Device): """do nothing""" @@ -86,6 +85,31 @@ class Ventilation(Device): def __init__(self): Device.__init__(self) self.is_small_vam = False # type: bool + self.capability = 0 # type: int + self.status = VentilationStatus() #type: VentilationStatus + +def get_device_by_vent(vent: Ventilation): + if vent.is_small_vam: + return EnumDevice.SMALL_VAM + else: + return EnumDevice.VENTILATION + +class VentilationStatus: + def __init__(self, + switch: EnumControl.Switch = None, + mode: EnumControl.Mode = None, + air_flow: EnumControl.AirFlow = None, + in_door_temp: int = None, + out_door_temp: int = None, + out_door_humidity: int = None, + pm25: int = None): + self.switch: EnumControl.Switch = switch + self.mode: EnumControl.Mode = mode + self.air_flow: EnumControl.AirFlow = air_flow + self.in_door_temp: int = in_door_temp + self.out_door_temp: int = out_door_temp + self.out_door_humidity: int = out_door_humidity + self.pm25: int = pm25 class HD(Device): @@ -148,4 +172,4 @@ def __init__(self): self.id = 0 # type: int self.name = '' # type: str self.type = 0 # type: int - self.ventilation = Ventilation() # type: Optional[Ventilation] + self.ventilation = None # type: Optional[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..6ccdc2b 100644 --- a/custom_components/ds_air/ds_air_service/decoder.py +++ b/custom_components/ds_air/ds_air_service/decoder.py @@ -1,14 +1,24 @@ +from cmath import log import struct import typing +import logging 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, \ +from .dao import Room, AirCon, Geothermic, Ventilation, HD, Device, AirConStatus, VentilationStatus, get_device_by_aircon, Sensor, \ UNINITIALIZED_VALUE from .param import GetRoomInfoParam, AirConRecommendedIndoorTempParam, AirConCapabilityQueryParam, \ - AirConQueryStatusParam, Sensor2InfoParam + AirConQueryStatusParam, Sensor2InfoParam, VentilationCapabilityQueryParam, VentilationQueryCompositeSituationParam, VentilationQueryStatusParam + +_LOGGER = logging.getLogger(__name__) + + +def _log(s: str): + s = str(s) + for i in s.split('\n'): + _LOGGER.warning(i) def decoder(b): @@ -42,8 +52,8 @@ def result_factory(data): result = LoginResult(cnt, EnumDevice.SYSTEM) elif cmd_type == EnumCmdType.SYS_CHANGE_PW.value: result = ChangePWResult(cnt, EnumDevice.SYSTEM) - elif cmd_type == EnumCmdType.SYS_GET_ROOM_INFO.value: - result = GetRoomInfoResult(cnt, EnumDevice.SYSTEM) + elif cmd_type == EnumCmdType.SYS_GET_ROOM_INFO.value or cmd_type == EnumCmdType.SYS_GET_ROOM_INFO_V1.value: + result = GetRoomInfoResult(cnt, EnumDevice.SYSTEM, EnumCmdType(cmd_type)) elif cmd_type == EnumCmdType.SYS_QUERY_SCHEDULE_SETTING.value: result = QueryScheduleSettingResult(cnt, EnumDevice.SYSTEM) elif cmd_type == EnumCmdType.SYS_QUERY_SCHEDULE_ID.value: @@ -58,6 +68,8 @@ def result_factory(data): result = ScheduleQueryVersionV3Result(cnt, EnumDevice.SYSTEM) elif cmd_type == EnumCmdType.SENSOR2_INFO: result = Sensor2InfoResult(cnt, EnumDevice.SYSTEM) + elif cmd_type == EnumCmdType.SYS_FILTER_CLEAN_SIGN: + result = FilterCleanSignResult(cnt, EnumDevice.SYSTEM) else: result = UnknownResult(cnt, EnumDevice.SYSTEM, cmd_type) elif dev_id == EnumDevice.NEWAIRCON.value[1] or dev_id == EnumDevice.AIRCON.value[1] \ @@ -77,6 +89,18 @@ def result_factory(data): result = Sensor2InfoResult(cnt, device) else: result = UnknownResult(cnt, device, cmd_type) + elif dev_id == EnumDevice.VENTILATION.value[1] or dev_id == EnumDevice.SMALL_VAM.value[1]: + device = EnumDevice((8, dev_id)) + if cmd_type == EnumCmdType.STATUS_CHANGED: + result = VentilationStatusChangedResult(cnt, device) + elif cmd_type == EnumCmdType.VENT_QUERY_CAPABILITY: + result = VentilationCapabilityQueryResult(cnt, device) + elif cmd_type == EnumCmdType.QUERY_STATUS.value: + result = VentilationQueryStatusResult(cnt, device) + elif cmd_type == EnumCmdType.SMALL_VAM_QUERY_COMPOSITE_SITUATION: + result = VentilationQueryCompositeSituationResult(cnt, device) + else: + result = UnknownResult(cnt, device, cmd_type) else: """ignore other device""" result = UnknownResult(cnt, EnumDevice.SYSTEM, cmd_type) @@ -88,7 +112,7 @@ def result_factory(data): class Decode: - def __init__(self, b): + def __init__(self, b: bytes): self._b = b self._pos = 0 @@ -106,6 +130,13 @@ def read2(self): self._pos = pos return s + def read_int16(self): + pos = self._pos + s = struct.unpack('> 2 & 1: self.air_flow = EnumControl.AirFlow(d.read1()) - if Config.is_c611: + if Config.is_c611 or Config.is_d611: if flag >> 3 & 1: bt = d.read1() self.hum_allow = bt & 8 == 8 @@ -797,3 +873,196 @@ def load_bytes(self, b): @property def subbody(self): return self._subbody + + +class VentilationStatusChangedResult(BaseResult): + def __init__(self, cmd_id: int, target: EnumDevice): + BaseResult.__init__(self, cmd_id, target, EnumCmdType.STATUS_CHANGED) + self._room = 0 # type: int + self._unit = 0 # type: int + self._status = VentilationStatus() # type: VentilationStatus + + def load_bytes(self, b): + d = Decode(b) + self._room = d.read1() + self._unit = d.read1() + status = self._status + flag = d.read1() + if flag & EnumControl.Type.SWITCH: + status.switch = EnumControl.Switch(d.read1()) + if flag & EnumControl.Type.MODE: + status.mode = EnumControl.Mode(d.read1()) + if flag & EnumControl.Type.AIR_FLOW: + status.air_flow = EnumControl.AirFlow(d.read1()) + + def do(self): + from .service import Service + Service.update_ventilation(self._room, self._unit, status=self._status) + +class VentilationCapabilityQueryResult(BaseResult): + def __init__(self, cmd_id: int, target: EnumDevice): + BaseResult.__init__(self, cmd_id, target, + EnumCmdType.VENT_QUERY_CAPABILITY) + self._vents: typing.List[Ventilation] = [] + + def load_bytes(self, b): + 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): + vent = Ventilation() + vent.unit_id = d.read1() + vent.room_id = room_id + vent.is_small_vam = self.target == EnumDevice.SMALL_VAM + self.data = bin(struct.unpack(' (typing.List[BaseResult], bytes): res.append(r) data = b except Exception as e: - _log(e) + _logError(e) data = None return res @@ -110,7 +115,7 @@ def run(self) -> None: if i is not None: i.do() except Exception as e: - _log(e) + _logError(e) self._locker.release() @@ -143,10 +148,12 @@ class Service: _aircons = None # type: typing.List[AirCon] _new_aircons = None # type: typing.List[AirCon] _bathrooms = None # type: typing.List[AirCon] + _ventilations = None # type: typing.List[Ventilation] _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)] + _status_hook = [] # type: typing.List[typing.Tuple[AirCon, typing.Callable]] + _sensor_hook = [] # type: typing.List[typing.Tuple[str, typing.Callable]] + _vent_hook = [] # type: typing.List[typing.Tuple[Ventilation, typing.Callable]] _heartbeat_thread = None _sensors = [] # type: typing.List[Sensor] _scan_interval = 5 # type: int @@ -161,7 +168,8 @@ def init(host: str, port: int, scan_interval: int): 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: + or Service._new_aircons is None or Service._bathrooms is None \ + or Service._ventilations is None: time.sleep(1) for i in Service._aircons: for j in Service._rooms: @@ -181,6 +189,12 @@ def init(host: str, port: int, scan_interval: int): i.alias = j.alias if i.unit_id: i.alias += str(i.unit_id) + for i in Service._ventilations: + for j in Service._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 @@ -193,7 +207,9 @@ def destroy(): Service._aircons = None Service._new_aircons = None Service._bathrooms = None + Service._ventilations = None Service._none_stat_dev_cnt = 0 + Service._vent_hook = [] Service._status_hook = [] Service._sensor_hook = [] Service._heartbeat_thread = None @@ -211,11 +227,20 @@ def get_aircons(): aircons += Service._bathrooms return aircons + @staticmethod + def get_ventilations(): + return Service._ventilations + @staticmethod def control(aircon: AirCon, status: AirConStatus): p = AirConControlParam(aircon, status) Service.send_msg(p) + @staticmethod + def control_vent(ventilation: Ventilation, status: VentilationStatus): + p = VentilationControlParam(ventilation, status) + Service.send_msg(p) + @staticmethod def register_status_hook(device: AirCon, hook: typing.Callable): Service._status_hook.append((device, hook)) @@ -224,6 +249,10 @@ def register_status_hook(device: AirCon, hook: typing.Callable): def register_sensor_hook(unique_id: str, hook: typing.Callable): Service._sensor_hook.append((unique_id, hook)) + @staticmethod + def register_vent_hook(device: Ventilation, hook: typing.Callable): + Service._vent_hook.append((device, hook)) + # ----split line---- above for component, below for inner call @staticmethod @@ -251,6 +280,10 @@ def get_sensors(): def set_sensors(sensors): Service._sensors = sensors + @staticmethod + def set_ventilations(ventilations): + Service._ventilations = ventilations + @staticmethod def set_device(t: EnumDevice, v: typing.List[AirCon]): Service._none_stat_dev_cnt += len(v) @@ -295,6 +328,20 @@ def set_sensors_status(sensors: typing.List[Sensor]): except Exception as e: _log(str(e)) + @staticmethod + def set_ventilation_status(room: int, unit: int, status: VentilationStatus): + if Service._ready: + Service.update_ventilation(room, unit, status=status) + else: + for i in Service._ventilations: + if i.unit_id == unit and i.room_id == room: + for attr in i.status.__dict__.keys(): + value = getattr(status, attr) + if value is not None and value != UNINITIALIZED_VALUE: + setattr(i.status, attr, value) + # Service._none_stat_dev_cnt -= 1 + break + @staticmethod def poll_status(): for i in Service._new_aircons: @@ -302,6 +349,16 @@ def poll_status(): p.target = EnumDevice.NEWAIRCON p.device = i Service.send_msg(p) + for v in Service._ventilations: + p = VentilationQueryStatusParam() + p.target = get_device_by_vent(v) + p.device = v + Service.send_msg(p) + if v.is_small_vam: + p = VentilationQueryCompositeSituationParam() + p.target = get_device_by_vent(v) + p.device = v + Service.send_msg(p) p = Sensor2InfoParam() Service.send_msg(p) @@ -317,6 +374,18 @@ def update_aircon(target: EnumDevice, room: int, unit: int, **kwargs): _log('hook error!!') _log(str(e)) + @staticmethod + def update_ventilation(room: int, unit: int, **kwargs): + li = Service._vent_hook + for item in li: + i, func = item + if i.unit_id == unit and i.room_id == room: + try: + func(**kwargs) + except Exception as e: + _logError('vent hook error!!') + _logError(str(e)) + @staticmethod def get_scan_interval(): return Service._scan_interval diff --git a/custom_components/ds_air/fan.py b/custom_components/ds_air/fan.py new file mode 100644 index 0000000..bfac2fa --- /dev/null +++ b/custom_components/ds_air/fan.py @@ -0,0 +1,231 @@ +"""Demo fan platform that has a fake fan.""" +from __future__ import annotations + +import logging +from operator import truediv +from re import S +from typing import Any,Optional, List + +from .ds_air_service.display import display +from .ds_air_service.ctrl_enum import _MODE_VENT_NAME_LIST_SMALL_VAM, _MODE_VENT_NAME_LIST_STANDARD_VAM, EnumControl + +from homeassistant.components.fan import FanEntity, FanEntityFeature +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import MAJOR_VERSION, MINOR_VERSION +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers.entity import DeviceInfo + +from .const import DOMAIN +from .ds_air_service.dao import Ventilation, VentilationStatus +from .ds_air_service.service import Service + +PRESET_MODE_AUTO = "auto" +PRESET_MODE_SMART = "smart" +PRESET_MODE_SLEEP = "sleep" +PRESET_MODE_ON = "on" + +FULL_SUPPORT = ( + FanEntityFeature.SET_SPEED | FanEntityFeature.OSCILLATE | FanEntityFeature.DIRECTION +) +LIMITED_SUPPORT = FanEntityFeature.SET_SPEED + +# TODO: Do Standard VAMs use FULL_SUPPORT or LIMITED_SUPPORT? +SMALL_VAM_SUPPORT = FanEntityFeature.SET_SPEED | FanEntityFeature.PRESET_MODE + +# For HA Core >= 2024.8, set TURN_ON and TURN_OFF flags for all VAMs. +# https://developers.home-assistant.io/blog/2024/07/19/fan-fanentityfeatures-turn-on_off/ +if (MAJOR_VERSION, MINOR_VERSION) >= (2024, 8): + POWER_SUPPORT = FanEntityFeature.TURN_ON | FanEntityFeature.TURN_OFF + + FULL_SUPPORT |= POWER_SUPPORT + LIMITED_SUPPORT |= POWER_SUPPORT + SMALL_VAM_SUPPORT |= POWER_SUPPORT + +_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, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + entities = [] + for vent in Service.get_ventilations(): + entities.append(DsVent(vent)) + async_add_entities(entities) + + +class DsVent(FanEntity): + """A demonstration fan component that uses legacy fan speeds.""" + + def __init__(self, vent: Ventilation): + _log('create ventilation:') + _log(vent.__dict__) + _log(vent.status) + """Initialize the climate device.""" + self._name = vent.alias + self._device_info = vent + self._unique_id = vent.unique_id + + # Don't include the AUTO mode. + if vent.is_small_vam: + self._attr_speed_count = len(_MODE_VENT_NAME_LIST_SMALL_VAM) - 1 + else: + self._attr_speed_count = len(_MODE_VENT_NAME_LIST_STANDARD_VAM) - 1 + Service.register_vent_hook(vent, self._status_change_hook) + + def _status_change_hook(self, **kwargs): + _log('hook:') + if kwargs.get('vent') is not None: + vent: Ventilation = kwargs['vent'] + vent.status = self._device_info.status + self._device_info = vent + _log(display(self._device_info)) + + if kwargs.get('status') is not None: + status = self._device_info.status + new_status: VentilationStatus = kwargs['status'] + if new_status.switch is not None: + status.switch = new_status.switch + if new_status.mode is not None: + status.mode = new_status.mode + if new_status.air_flow is not None: + status.air_flow = new_status.air_flow + _log('new status') + _log(display(kwargs['status'])) + _log('updated status') + _log(display(self._device_info.status)) + + self.schedule_update_ha_state() + + @property + def unique_id(self) -> str: + """Return the unique id.""" + return self._unique_id + + @property + def name(self) -> str: + """Get entity name.""" + return self._name + + @property + def should_poll(self) -> bool: + """No polling needed for a demo fan.""" + return False + + @property + def supported_features(self) -> int: + """Flag supported features.""" + # TODO: Do Standard VAMs use FULL_SUPPORT or LIMITED_SUPPORT? + return SMALL_VAM_SUPPORT + + @property + def percentage(self) -> int | None: + vent = self._device_info + if vent.status.air_flow is None: + return None + + if vent.is_small_vam: + return vent.status.air_flow.value * self.percentage_step + else: + if vent.status.air_flow == EnumControl.AirFlow.WEAK: + return 50 + elif vent.status.air_flow == EnumControl.AirFlow.STRONG: + return 100 + + return None + + def set_percentage(self, percentage: int) -> None: + vent = self._device_info + new_status = VentilationStatus() + + if vent.is_small_vam: + air_flow = EnumControl.AirFlow(round(percentage / self.percentage_step)) + else: + if percentage > 50: + air_flow = EnumControl.AirFlow.STRONG + elif percentage > 0: + air_flow = EnumControl.AirFlow.WEAK + else: + air_flow = vent.status.air_flow + + if percentage > 0 and vent.status.switch != EnumControl.Switch.ON: + vent.status.switch = EnumControl.Switch.ON + new_status.switch = EnumControl.Switch.ON + + vent.status.air_flow = air_flow + if air_flow != EnumControl.AirFlow.SUPER_WEAK: + new_status.air_flow = air_flow + Service.control_vent(self._device_info, new_status) + self.schedule_update_ha_state() + + def set_preset_mode(self, preset_mode: str) -> None: + vent = self._device_info + status = vent.status + new_status = VentilationStatus() + if vent.is_small_vam: + mode = EnumControl.get_vent_mode_enum_small_vam(preset_mode) + else: + mode = EnumControl.get_vent_mode_enum_standard_vam(preset_mode) + status.mode = mode + new_status.mode = mode + Service.control_vent(self._device_info, new_status) + + @property + def preset_mode(self) -> str | None: + if self._device_info.status.mode is None: + return None + elif self._device_info.is_small_vam: + return EnumControl.get_vent_mode_name_small_vam(self._device_info.status.mode) + else: + return EnumControl.get_vent_mode_name_standard_vam(self._device_info.status.mode) + + @property + def preset_modes(self) -> list[str] | None: + if self._device_info.is_small_vam: + return _MODE_VENT_NAME_LIST_SMALL_VAM + else: + return _MODE_VENT_NAME_LIST_STANDARD_VAM + + @property + def device_info(self) -> Optional[DeviceInfo]: + return { + "identifiers": {(DOMAIN, self._unique_id)}, + "name": "新风%s" % self._name, + "manufacturer": "Daikin Industries, Ltd." + } + + @property + def is_on(self) -> bool | None: + """Return true if device is on.""" + if self._device_info.status.switch is None: + return None + return self._device_info.status.switch == EnumControl.Switch.ON + + def turn_on(self, **kwargs: Any) -> None: + """Turn on the fan.""" + vent = self._device_info + status = vent.status + new_status = VentilationStatus() + status.switch = EnumControl.Switch.ON + new_status.switch = EnumControl.Switch.ON + + Service.control_vent(self._device_info, new_status) + # self._switch = True + self.schedule_update_ha_state() + + def turn_off(self, **kwargs: Any) -> None: + """Turn the fan off.""" + vent = self._device_info + status = vent.status + new_status = VentilationStatus() + status.switch = EnumControl.Switch.OFF + new_status.switch = EnumControl.Switch.OFF + Service.control_vent(self._device_info, new_status) + self.schedule_update_ha_state() diff --git a/custom_components/ds_air/sensor.py b/custom_components/ds_air/sensor.py index d211b98..1e63131 100644 --- a/custom_components/ds_air/sensor.py +++ b/custom_components/ds_air/sensor.py @@ -4,8 +4,8 @@ from homeassistant.components.sensor import SensorEntity, SensorStateClass from homeassistant.helpers.entity import DeviceInfo -from .const import DOMAIN, SENSOR_TYPES -from .ds_air_service.dao import Sensor, UNINITIALIZED_VALUE +from .const import DOMAIN, SENSOR_TYPES, SMALL_VAM_SENSOR_TYPES +from .ds_air_service.dao import Sensor, UNINITIALIZED_VALUE, Ventilation, VentilationStatus from .ds_air_service.service import Service @@ -16,6 +16,12 @@ async def async_setup_entry(hass, config_entry, async_add_entities): for key in SENSOR_TYPES: if config_entry.data.get(key): entities.append(DsSensor(device, key)) + + for vent in Service.get_ventilations(): + if vent.is_small_vam: + for key in SMALL_VAM_SENSOR_TYPES: + entities.append(DsVentSensor(vent, key)) + async_add_entities(entities) @@ -80,7 +86,7 @@ def device_class(self): if self._data_key in SENSOR_TYPES else None ) - + @property def state_class(self): """Return the state class of this entity.""" @@ -104,3 +110,90 @@ def parse_data(self, device: Sensor, not_update: bool = False): if not not_update: self.schedule_update_ha_state() return True + +class DsVentSensor(SensorEntity): + """Representation of a sensor from DaikinVentilation.""" + def __init__(self, device: Ventilation, data_key: str): + """Initialize the sensor from DaikinVentilation.""" + self._data_key = data_key + self._name = device.alias + self._unique_id = device.unique_id + self._is_available = False + self._state = 0 + self._device = device + self.parse_data(device.status, True) + Service.register_vent_hook(device, 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) + + @property + def device_info(self) -> Optional[DeviceInfo]: + return { + "identifiers": {(DOMAIN, self._unique_id)}, + "name": "新风%s" % self._name, + "manufacturer": "Daikin Industries, Ltd." + } + + @property + def available(self): + return self._is_available + + @property + def should_poll(self): + return False + + @property + def icon(self): + """Return the icon to use in the frontend.""" + try: + return SMALL_VAM_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 SMALL_VAM_SENSOR_TYPES.get(self._data_key)[0] + except TypeError: + return None + + @property + def device_class(self): + """Return the device class of this entity.""" + return ( + SMALL_VAM_SENSOR_TYPES.get(self._data_key)[2] + if self._data_key in SMALL_VAM_SENSOR_TYPES + else None + ) + + @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 + + def parse_data(self, status: VentilationStatus, not_update: bool = False): + """Parse data sent by gateway.""" + value = getattr(status, self._data_key) + if value is not None and UNINITIALIZED_VALUE != value: + self._is_available = True + setattr(self._device.status, self._data_key, value) + if type(SMALL_VAM_SENSOR_TYPES.get(self._data_key)[3]) != int: + self._state = str(value) + else: + self._state = value / SMALL_VAM_SENSOR_TYPES.get(self._data_key)[3] + + if not not_update: + self.schedule_update_ha_state() + return True