From 9fd3f74fbf6da4116c8016d895dce7a30cd60e74 Mon Sep 17 00:00:00 2001 From: mypal Date: Fri, 28 Jun 2019 19:52:03 +0800 Subject: [PATCH 01/16] Update README.md --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f51ab56..6c48956 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,10 @@ climate: ``` 3. 重启HA服务 +# TODO + +根据APP反解显示,网关可控制新风、地暖、HD(不知道是个啥设备)、新版空调、老版空调和浴室设备。由于我家只有新版室内机,所以目前只实现了这个。其他设备实现没写完,理论上都不能够支持。 + # 开发过程 -本组件开发过程可在[blog](https://www.mypal.wang/blog/lun-yi-ci-jia-yong-kong-diao-jie-ru-hazhe-teng-jing-li/)查看 \ No newline at end of file +本组件开发过程可在[blog](https://www.mypal.wang/blog/lun-yi-ci-jia-yong-kong-diao-jie-ru-hazhe-teng-jing-li/)查看 From e602fe40b2f24848aae68a40ee89dba2e55862b0 Mon Sep 17 00:00:00 2001 From: ypwub5 Date: Fri, 19 Aug 2022 16:58:58 +0800 Subject: [PATCH 02/16] Ventilator detection and power control --- custom_components/ds_air/__init__.py | 2 +- .../ds_air/ds_air_service/dao.py | 6 + .../ds_air/ds_air_service/decoder.py | 101 ++++++++++++++- .../ds_air/ds_air_service/param.py | 71 +++++++++- .../ds_air/ds_air_service/service.py | 70 +++++++++- custom_components/ds_air/fan.py | 122 ++++++++++++++++++ 6 files changed, 364 insertions(+), 8 deletions(-) create mode 100644 custom_components/ds_air/fan.py diff --git a/custom_components/ds_air/__init__.py b/custom_components/ds_air/__init__.py index ca592ea..f643de6 100644 --- a/custom_components/ds_air/__init__.py +++ b/custom_components/ds_air/__init__.py @@ -13,7 +13,7 @@ from .ds_air_service.config import Config _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["climate", "sensor"] +PLATFORMS = ["climate", "sensor", "fan"] def _log(s: str): diff --git a/custom_components/ds_air/ds_air_service/dao.py b/custom_components/ds_air/ds_air_service/dao.py index 38d6350..5bcb420 100644 --- a/custom_components/ds_air/ds_air_service/dao.py +++ b/custom_components/ds_air/ds_air_service/dao.py @@ -85,8 +85,14 @@ class Geothermic(Device): class Ventilation(Device): def __init__(self): Device.__init__(self) + self.switch = EnumControl.Switch.OFF # type: EnumControl.Switch self.is_small_vam = False # type: bool +class VentilationStatus: + def __init__(self, + switch: EnumControl.Switch = None): + self.switch = switch # type: EnumControl.Switch + class HD(Device): def __init__(self): diff --git a/custom_components/ds_air/ds_air_service/decoder.py b/custom_components/ds_air/ds_air_service/decoder.py index 795d7ea..80a68e3 100644 --- a/custom_components/ds_air/ds_air_service/decoder.py +++ b/custom_components/ds_air/ds_air_service/decoder.py @@ -5,10 +5,10 @@ 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, VentilationQueryStatusParam def decoder(b): @@ -78,6 +78,16 @@ def result_factory(data): result = Sensor2InfoResult(cnt, device) else: result = UnknownResult(cnt, device, cmd_type) + elif dev_id == EnumDevice.VENTILATION.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) + else: + result = UnknownResult(cnt, device, cmd_type) else: """ignore other device""" result = UnknownResult(cnt, EnumDevice.SYSTEM, cmd_type) @@ -470,6 +480,7 @@ def do(self): aircons = [] new_aircons = [] bathrooms = [] + ventilations = [] for room in Service.get_rooms(): if room.air_con is not None: room.air_con.alias = room.alias @@ -479,6 +490,8 @@ def do(self): bathrooms.append(room.air_con) else: aircons.append(room.air_con) + elif room.ventilation is not None: + ventilations.append(room.ventilation) p = AirConCapabilityQueryParam() p.aircons = aircons @@ -492,6 +505,10 @@ def do(self): p.aircons = bathrooms p.target = EnumDevice.BATHROOM Service.send_msg(p) + p = VentilationCapabilityQueryParam() + p.vents = ventilations + p.target = EnumDevice.VENTILATION + Service.send_msg(p) @property def count(self): @@ -793,3 +810,83 @@ 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()) + + 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] = [] + self.target = EnumDevice.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 + flag = d.read1() + if flag & EnumControl.Type.SWITCH: + vent.switch = EnumControl.Switch(d.read1()) + self._vents.append(vent) + + def do(self): + from .service import Service + if Service.is_ready(): + if len(self._vents): + for i in self._vents: + Service.update_ventilation(i.room_id, i.unit_id, vent=i) + else: + for vent in self._vents: + p = VentilationQueryStatusParam() + p.target = self.target + p.device = vent + from .service import Service + Service.send_msg(p) + Service.set_ventilations(self._vents) + + # @property + # def aircons(self): + # return self._air_cons + +class VentilationQueryStatusResult(BaseResult): + def __init__(self, cmd_id: int, target: EnumDevice): + BaseResult.__init__(self, cmd_id, target, EnumCmdType.QUERY_STATUS) + self.unit_id = 0 + self.room_id = 0 + self.switch = EnumControl.Switch.OFF + + def load_bytes(self, b): + d = Decode(b) + self.room_id = d.read1() + self.unit_id = d.read1() + flag = d.read1() + if flag & EnumControl.Type.SWITCH: + self.switch = EnumControl.Switch(d.read1()) + + def do(self): + from .service import Service + status = VentilationStatus(self.switch) + Service.set_ventilation_status(self.room_id, self.unit_id, status) \ No newline at end of file diff --git a/custom_components/ds_air/ds_air_service/param.py b/custom_components/ds_air/ds_air_service/param.py index 8622778..2cf3b9f 100644 --- a/custom_components/ds_air/ds_air_service/param.py +++ b/custom_components/ds_air/ds_air_service/param.py @@ -3,7 +3,7 @@ from typing import Optional from .config import Config -from .dao import AirCon, Device, get_device_by_aircon, AirConStatus +from .dao import AirCon, Device, get_device_by_aircon, AirConStatus, Ventilation, VentilationStatus from .base_bean import BaseBean from .ctrl_enum import EnumCmdType, EnumDevice, EnumControl, EnumFanDirection, EnumFanVolume @@ -243,3 +243,72 @@ def generate_subbody(self, s): s.write1(val) elif bit == 2: s.write2(val) + +class VentilationParam(Param): + def __init__(self, cmd_cype, has_result): + Param.__init__(self, EnumDevice.VENTILATION, cmd_cype, has_result) + +class VentilationCapabilityQueryParam(VentilationParam): + def __init__(self): + VentilationParam.__init__(self, EnumCmdType.VENT_QUERY_CAPABILITY, True) + self._vents: typing.List[Ventilation] = [] + + def generate_subbody(self, s): + s.write1(len(self._vents)) + for i in self._vents: + s.write1(i.room_id) + s.write1(1) + s.write1(0) + + @property + def vents(self): + return self._vents + + @vents.setter + def vents(self, value): + self._vents = value + +class VentilationQueryStatusParam(VentilationParam): + def __init__(self): + super().__init__(EnumCmdType.QUERY_STATUS, True) + self._device = None # type: Optional[Ventilation] + + def generate_subbody(self, s): + s.write1(self._device.room_id) + s.write1(self._device.unit_id) + t = EnumControl.Type + flag = t.SWITCH + # dev = self.device + s.write1(flag) + + @property + def device(self): + return self._device + + @device.setter + def device(self, v: Ventilation): + self._device = v + +class VentilationControlParam(VentilationParam): + def __init__(self, vent: Ventilation, new_status: VentilationStatus): + super().__init__(EnumCmdType.CONTROL, False) + self.target = EnumDevice.VENTILATION + self._vent = vent + self._new_status = new_status + + def generate_subbody(self, s): + vent = self._vent + status = self._new_status + s.write1(vent.room_id) + s.write1(vent.unit_id) + li = [] + flag = 0 + if status.switch is not None: + flag = flag | EnumControl.Type.SWITCH + li.append((1, status.switch.value)) + s.write1(flag) + for bit, val in li: + if bit == 1: + s.write1(val) + elif bit == 2: + s.write2(val) \ No newline at end of file diff --git a/custom_components/ds_air/ds_air_service/service.py b/custom_components/ds_air/ds_air_service/service.py index 5c148ea..1a05f81 100644 --- a/custom_components/ds_air/ds_air_service/service.py +++ b/custom_components/ds_air/ds_air_service/service.py @@ -4,11 +4,11 @@ import typing from threading import Thread, Lock -from .ctrl_enum import EnumDevice -from .dao import Room, AirCon, AirConStatus, get_device_by_aircon, Sensor, STATUS_ATTR +from .ctrl_enum import EnumDevice, EnumControl +from .dao import Room, AirCon, AirConStatus, Ventilation, VentilationStatus, get_device_by_aircon, Sensor, STATUS_ATTR from .decoder import decoder, BaseResult from .display import display -from .param import Param, HandShakeParam, HeartbeatParam, AirConControlParam, AirConQueryStatusParam, Sensor2InfoParam +from .param import Param, HandShakeParam, HeartbeatParam, AirConControlParam, AirConQueryStatusParam, Sensor2InfoParam, VentilationQueryStatusParam, VentilationControlParam _LOGGER = logging.getLogger(__name__) @@ -142,10 +142,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)] + _vent_hook = [] # type: typing.List[(Ventilation, typing.Callable)] _heartbeat_thread = None _sensors = [] # type: typing.List[Sensor] _scan_interval = 5 # type: int @@ -160,7 +162,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: @@ -180,6 +183,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 @@ -192,7 +201,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 @@ -203,11 +214,25 @@ def destroy(): def get_aircons(): return Service._new_aircons+Service._aircons+Service._bathrooms + @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(aircon: Ventilation, switch: bool): + statusVent = VentilationStatus() + if switch == True: + statusVent.switch = EnumControl.Switch(1) + else: + statusVent.switch = EnumControl.Switch(0) + p = VentilationControlParam(Service._ventilations[0], statusVent) + Service.send_msg(p) + @staticmethod def register_status_hook(device: AirCon, hook: typing.Callable): Service._status_hook.append((device, hook)) @@ -216,6 +241,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 @@ -243,6 +272,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) @@ -287,6 +320,18 @@ 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: + i.status = status + i.switch = status.switch + # Service._none_stat_dev_cnt -= 1 + break + @staticmethod def poll_status(): for i in Service._new_aircons: @@ -294,6 +339,11 @@ def poll_status(): p.target = EnumDevice.NEWAIRCON p.device = i Service.send_msg(p) + for v in Service._ventilations: + p = VentilationQueryStatusParam() + p.target = EnumDevice.VENTILATION + p.device = v + Service.send_msg(p) p = Sensor2InfoParam() Service.send_msg(p) @@ -309,6 +359,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: + _log('vent hook error!!') + _log(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..9bb8f61 --- /dev/null +++ b/custom_components/ds_air/fan.py @@ -0,0 +1,122 @@ +"""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.ctrl_enum import EnumControl + +from homeassistant.components.fan import FanEntity, FanEntityFeature +from homeassistant.config_entries import ConfigEntry +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 + +_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(str(vent.__dict__)) + _log(str(vent.switch)) + """Initialize the climate device.""" + self._name = vent.alias + self._device_info = vent + self._unique_id = vent.unique_id + self._switch = vent.switch + from .ds_air_service.service import Service + 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'] + self._device_info = vent + self._switch = vent.switch + + if kwargs.get('status') is not None: + new_status: VentilationStatus = kwargs['status'] + if new_status.switch is not None: + self._switch = new_status.switch + 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.""" + return 0 + + @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.""" + return self._switch == EnumControl.Switch.ON + + def turn_on(self, **kwargs: Any) -> None: + """Turn on the fan.""" + from .ds_air_service.service import Service + Service.control_vent(self._device_info, True) + # self._switch = True + self.schedule_update_ha_state() + + def turn_off(self, **kwargs: Any) -> None: + """Turn the fan off.""" + from .ds_air_service.service import Service + Service.control_vent(self._device_info, False) + # self._switch = False + self.schedule_update_ha_state() \ No newline at end of file From a9c504c8224dd7d88494ac3001edf27a0eca649c Mon Sep 17 00:00:00 2001 From: ypwub5 Date: Tue, 20 Sep 2022 16:29:07 +0800 Subject: [PATCH 03/16] small vam test --- .../ds_air/ds_air_service/decoder.py | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/custom_components/ds_air/ds_air_service/decoder.py b/custom_components/ds_air/ds_air_service/decoder.py index 80a68e3..0ecf82c 100644 --- a/custom_components/ds_air/ds_air_service/decoder.py +++ b/custom_components/ds_air/ds_air_service/decoder.py @@ -1,5 +1,7 @@ +from cmath import log import struct import typing +import logging from .base_bean import BaseBean from .config import Config @@ -10,6 +12,13 @@ from .param import GetRoomInfoParam, AirConRecommendedIndoorTempParam, AirConCapabilityQueryParam, \ AirConQueryStatusParam, Sensor2InfoParam, VentilationCapabilityQueryParam, VentilationQueryStatusParam +_LOGGER = logging.getLogger(__name__) + + +def _log(s: str): + s = str(s) + for i in s.split('\n'): + _LOGGER.warning(i) def decoder(b): if b[0] != 2: @@ -481,6 +490,7 @@ def do(self): new_aircons = [] bathrooms = [] ventilations = [] + smallVAM = [] for room in Service.get_rooms(): if room.air_con is not None: room.air_con.alias = room.alias @@ -491,7 +501,12 @@ def do(self): else: aircons.append(room.air_con) elif room.ventilation is not None: - ventilations.append(room.ventilation) + if room.ventilation.is_small_vam: + smallVAM.append(room.ventilation) + _log('ds_air ---> small vam found') + else: + ventilations.append(room.ventilation) + _log('ds_air ---> big vam found') p = AirConCapabilityQueryParam() p.aircons = aircons @@ -510,6 +525,10 @@ def do(self): p.target = EnumDevice.VENTILATION Service.send_msg(p) + # no ventilator detected + if smallVAM.count == 0 and ventilations.count == 0: + Service.set_ventilations([]) + @property def count(self): return self._count From d504899dc40bf162d543434c8081194e30a1e654 Mon Sep 17 00:00:00 2001 From: lightrabbit Date: Sat, 7 Oct 2023 22:41:08 +0800 Subject: [PATCH 04/16] feat: add suport for smallVAM --- .../ds_air/ds_air_service/ctrl_enum.py | 21 ++- .../ds_air/ds_air_service/dao.py | 18 +- .../ds_air/ds_air_service/decoder.py | 170 +++++++++++++++--- .../ds_air/ds_air_service/param.py | 47 +++-- .../ds_air/ds_air_service/service.py | 19 +- custom_components/ds_air/fan.py | 79 +++++++- 6 files changed, 298 insertions(+), 56 deletions(-) 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 eab9811..8fae702 100644 --- a/custom_components/ds_air/ds_air_service/ctrl_enum.py +++ b/custom_components/ds_air/ds_air_service/ctrl_enum.py @@ -148,7 +148,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 @@ -236,6 +236,7 @@ class AirFlow(IntEnum): _AIR_FLOW_NAME_LIST = ['最弱', '稍弱', '中等', '稍强', '最强', '自动'] +_VENT_AIR_FLOW_NAME_LIST = ['INVALID', '静音', '中速', '高速', '暴风'] class Breathe(IntEnum): @@ -293,7 +294,7 @@ class Mode(IntEnum): _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_VENT_NAME_LIST = ["内循环", "热交换", "自动", "防污染", "排异味"] class Switch(IntEnum): OFF = 0 @@ -331,6 +332,14 @@ def get_mode_name(idx): @staticmethod def get_mode_enum(name): return Mode(_MODE_NAME_LIST.index(name)) + + @staticmethod + def get_vent_mode_name(idx): + return _MODE_VENT_NAME_LIST[idx] + + @staticmethod + def get_vent_mode_enum(name: str): + return Mode(_MODE_VENT_NAME_LIST.index(name)) @staticmethod def get_air_flow_name(idx): @@ -339,6 +348,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 5bcb420..3a23777 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""" @@ -85,13 +84,24 @@ class Geothermic(Device): class Ventilation(Device): def __init__(self): Device.__init__(self) - self.switch = EnumControl.Switch.OFF # type: EnumControl.Switch 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): + switch: EnumControl.Switch = None, + mode: EnumControl.Mode = None, + air_flow: EnumControl.AirFlow = None): self.switch = switch # type: EnumControl.Switch + self.mode = mode # type: EnumControl.Mode + self.air_flow = air_flow # type: EnumControl.AirFlow class HD(Device): @@ -154,4 +164,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 2368e3f..909908f 100644 --- a/custom_components/ds_air/ds_air_service/decoder.py +++ b/custom_components/ds_air/ds_air_service/decoder.py @@ -10,7 +10,7 @@ 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, VentilationCapabilityQueryParam, VentilationQueryStatusParam + AirConQueryStatusParam, Sensor2InfoParam, VentilationCapabilityQueryParam, VentilationQueryCompositeSituationParam, VentilationQueryStatusParam _LOGGER = logging.getLogger(__name__) @@ -20,6 +20,7 @@ def _log(s: str): for i in s.split('\n'): _LOGGER.warning(i) + def decoder(b): if b[0] != 2: return None, None @@ -51,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: @@ -67,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] \ @@ -86,7 +89,7 @@ def result_factory(data): result = Sensor2InfoResult(cnt, device) else: result = UnknownResult(cnt, device, cmd_type) - elif dev_id == EnumDevice.VENTILATION.value[1]: + 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) @@ -94,6 +97,8 @@ def result_factory(data): 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: @@ -107,7 +112,7 @@ def result_factory(data): class Decode: - def __init__(self, b): + def __init__(self, b: bytes): self._b = b self._pos = 0 @@ -149,12 +154,11 @@ def read_utf(self, l): self._pos = pos return s - class BaseResult(BaseBean): def __init__(self, cmd_id: int, targe: EnumDevice, cmd_type: EnumCmdType): BaseBean.__init__(self, cmd_id, targe, cmd_type) - def load_bytes(self, b): + def load_bytes(self, b: bytes): """do nothing""" def do(self): @@ -179,6 +183,23 @@ def __init__(self, cmd_id: int, target: EnumDevice): BaseResult.__init__(self, cmd_id, target, EnumCmdType.SYS_ACK) +class FilterCleanSignResult(BaseResult): + def __init__(self, cmd_id: int, target: EnumDevice): + BaseResult.__init__(self, cmd_id, target, + EnumCmdType.SYS_FILTER_CLEAN_SIGN) + self.a = 0 + self.b = 0 + self.c = 0 + self.d = 0 + + def load_bytes(self, b): + data = Decode(b) + self.a = data.read4() + self.b = data.read1() + self.c = data.read1() + self.d = data.read1() + + class Sensor2InfoResult(BaseResult): def __init__(self, cmd_id: int, target: EnumDevice): BaseResult.__init__(self, cmd_id, target, EnumCmdType.SENSOR2_INFO) @@ -421,8 +442,11 @@ def status(self): class GetRoomInfoResult(BaseResult): - def __init__(self, cmd_id: int, target: EnumDevice): - BaseResult.__init__(self, cmd_id, target, EnumCmdType.SYS_GET_ROOM_INFO) + def __init__(self, cmd_id: int, + target: EnumDevice, + cmd_type: typing.Literal[EnumCmdType.SYS_GET_ROOM_INFO, + EnumCmdType.SYS_GET_ROOM_INFO_V1]): + BaseResult.__init__(self, cmd_id, target, cmd_type) self._count: int = 0 self._hds: typing.List[HD] = [] self._sensors: typing.List[Sensor] = [] @@ -487,7 +511,11 @@ def load_bytes(self, b): def do(self): from .service import Service Service.set_rooms(self.rooms) - Service.send_msg(AirConRecommendedIndoorTempParam()) + + if not Config.is_new_version: + # DTA117D611 似乎不支持这个参数,在路由器上抓到了它上报不支持这个请求的信息 + Service.send_msg(AirConRecommendedIndoorTempParam()) + Service.set_sensors(self.sensors) aircons = [] @@ -507,10 +535,8 @@ def do(self): elif room.ventilation is not None: if room.ventilation.is_small_vam: smallVAM.append(room.ventilation) - _log('ds_air ---> small vam found') else: ventilations.append(room.ventilation) - _log('ds_air ---> big vam found') p = AirConCapabilityQueryParam() p.aircons = aircons @@ -528,6 +554,10 @@ def do(self): p.vents = ventilations p.target = EnumDevice.VENTILATION Service.send_msg(p) + p = VentilationCapabilityQueryParam() + p.vents = smallVAM + p.target = EnumDevice.SMALL_VAM + Service.send_msg(p) # no ventilator detected if smallVAM.count == 0 and ventilations.count == 0: @@ -576,7 +606,10 @@ def load_bytes(self, b): self._time = d.read_utf(14) def do(self): - p = GetRoomInfoParam() + if Config.is_new_version: + p = GetRoomInfoParam(EnumCmdType.SYS_GET_ROOM_INFO_V1) + else: + p = GetRoomInfoParam(EnumCmdType.SYS_GET_ROOM_INFO) p.room_ids.append(0xffff) from .service import Service Service.send_msg(p) @@ -834,6 +867,7 @@ def load_bytes(self, b): 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) @@ -849,6 +883,10 @@ def load_bytes(self, b): 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 @@ -856,9 +894,9 @@ def do(self): class VentilationCapabilityQueryResult(BaseResult): def __init__(self, cmd_id: int, target: EnumDevice): - BaseResult.__init__(self, cmd_id, target, EnumCmdType.VENT_QUERY_CAPABILITY) + BaseResult.__init__(self, cmd_id, target, + EnumCmdType.VENT_QUERY_CAPABILITY) self._vents: typing.List[Ventilation] = [] - self.target = EnumDevice.VENTILATION def load_bytes(self, b): d = Decode(b) @@ -870,9 +908,19 @@ def load_bytes(self, b): 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(' bool: @property def supported_features(self) -> int: """Flag supported features.""" + if self._device_info.is_small_vam: + return SMALL_VAM_SUPPORT return 0 + + @property + def percentage(self) -> int | None: + vent = self._device_info + if vent.status.air_flow is None: + return None + return vent.status.air_flow * self.percentage_step + + def set_percentage(self, percentage: int) -> None: + vent = self._device_info + status = vent.status + new_status = VentilationStatus() + air_flow = round(percentage / self.percentage_step) + status.air_flow = air_flow + new_status.air_flow = air_flow + from .ds_air_service.service import Service + Service.control_vent(self._device_info, new_status) + + def set_preset_mode(self, preset_mode: str) -> None: + vent = self._device_info + status = vent.status + new_status = VentilationStatus() + mode = EnumControl.get_vent_mode_enum(preset_mode) + status.mode = mode + new_status.mode = mode + from .ds_air_service.service import Service + 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 + return EnumControl.get_vent_mode_name(self._device_info.status.mode) + + @property + def preset_modes(self) -> list[str] | None: + return _MODE_VENT_NAME_LIST @property def device_info(self) -> Optional[DeviceInfo]: @@ -105,18 +156,30 @@ def device_info(self) -> Optional[DeviceInfo]: @property def is_on(self) -> bool | None: """Return true if device is on.""" - return self._switch == EnumControl.Switch.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 + from .ds_air_service.service import Service - Service.control_vent(self._device_info, True) + 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.""" from .ds_air_service.service import Service - Service.control_vent(self._device_info, False) - # self._switch = False + 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() \ No newline at end of file From 304bef8f72a70d4aab2346344dc9a13a98192a36 Mon Sep 17 00:00:00 2001 From: lightrabbit Date: Sat, 7 Oct 2023 23:39:16 +0800 Subject: [PATCH 05/16] fix: device info --- custom_components/ds_air/fan.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/ds_air/fan.py b/custom_components/ds_air/fan.py index fd3434b..f08ffa6 100644 --- a/custom_components/ds_air/fan.py +++ b/custom_components/ds_air/fan.py @@ -148,9 +148,9 @@ def preset_modes(self) -> list[str] | None: @property def device_info(self) -> Optional[DeviceInfo]: return { - "identifiers": {(DOMAIN, self.unique_id)}, + "identifiers": {(DOMAIN, self._unique_id)}, "name": "新风%s" % self._name, - "manufacturer": "DAIKIN INDUSTRIES, Ltd." + "manufacturer": "Daikin Industries, Ltd." } @property From 117ddedcebd688792b3e08bdf9f7dff2dcd3133a Mon Sep 17 00:00:00 2001 From: lightrabbit Date: Sun, 8 Oct 2023 02:28:30 +0800 Subject: [PATCH 06/16] fix: bug --- custom_components/ds_air/ds_air_service/param.py | 4 ++-- custom_components/ds_air/fan.py | 11 ++++++----- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/custom_components/ds_air/ds_air_service/param.py b/custom_components/ds_air/ds_air_service/param.py index 5d1be84..b37c362 100644 --- a/custom_components/ds_air/ds_air_service/param.py +++ b/custom_components/ds_air/ds_air_service/param.py @@ -311,10 +311,10 @@ def generate_subbody(self, s): li.append((1, status.switch.value)) if status.mode is not None: flag = flag | EnumControl.Type.MODE - li.append((1, status.switch.value)) + li.append((1, status.mode.value)) if status.air_flow is not None: flag = flag | EnumControl.Type.AIR_FLOW - li.append((1, status.switch.value)) + li.append((1, status.air_flow.value)) s.write1(flag) for bit, val in li: diff --git a/custom_components/ds_air/fan.py b/custom_components/ds_air/fan.py index f08ffa6..de88563 100644 --- a/custom_components/ds_air/fan.py +++ b/custom_components/ds_air/fan.py @@ -113,17 +113,18 @@ def percentage(self) -> int | None: vent = self._device_info if vent.status.air_flow is None: return None - return vent.status.air_flow * self.percentage_step + return vent.status.air_flow.value * self.percentage_step def set_percentage(self, percentage: int) -> None: vent = self._device_info status = vent.status new_status = VentilationStatus() - air_flow = round(percentage / self.percentage_step) + air_flow = EnumControl.AirFlow(round(percentage / self.percentage_step)) status.air_flow = air_flow - new_status.air_flow = air_flow - from .ds_air_service.service import Service - Service.control_vent(self._device_info, new_status) + if air_flow != EnumControl.AirFlow.SUPER_WEAK: + new_status.air_flow = air_flow + from .ds_air_service.service import Service + Service.control_vent(self._device_info, new_status) def set_preset_mode(self, preset_mode: str) -> None: vent = self._device_info From b4d48061588d7ed39f91545f0c9054417d1bbe89 Mon Sep 17 00:00:00 2001 From: lightrabbit Date: Sat, 21 Oct 2023 22:00:34 +0800 Subject: [PATCH 07/16] =?UTF-8?q?fix:=20=E7=BD=91=E5=85=B3=E5=85=BC?= =?UTF-8?q?=E5=AE=B9=E6=80=A7=E9=97=AE=E9=A2=98=20B611=E5=BA=94=E8=AF=A5?= =?UTF-8?q?=E4=BD=BF=E7=94=A8=20`SYS=5FGET=5FROOM=5FINFO`=E8=80=8C?= =?UTF-8?q?=E4=B8=8D=E6=98=AF=20`SYS=5FGET=5FROOM=5FINFO=5FV1`=20=E6=9D=A5?= =?UTF-8?q?=E8=8E=B7=E5=8F=96=E6=88=BF=E9=97=B4=E4=BF=A1=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- custom_components/ds_air/ds_air_service/decoder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/ds_air/ds_air_service/decoder.py b/custom_components/ds_air/ds_air_service/decoder.py index 909908f..53fd069 100644 --- a/custom_components/ds_air/ds_air_service/decoder.py +++ b/custom_components/ds_air/ds_air_service/decoder.py @@ -606,7 +606,7 @@ def load_bytes(self, b): self._time = d.read_utf(14) def do(self): - if Config.is_new_version: + if Config.is_new_version and Config.is_c611: p = GetRoomInfoParam(EnumCmdType.SYS_GET_ROOM_INFO_V1) else: p = GetRoomInfoParam(EnumCmdType.SYS_GET_ROOM_INFO) From 863efbcc419910ac1f87c4dd74b7c37ed36ac7cf Mon Sep 17 00:00:00 2001 From: lightrabbit Date: Sun, 22 Oct 2023 01:38:41 +0800 Subject: [PATCH 08/16] =?UTF-8?q?feat:=20=E8=AE=A9=E6=A0=87=E5=87=86VAM?= =?UTF-8?q?=E4=B9=9F=E6=94=AF=E6=8C=81=E4=BF=AE=E6=94=B9=E6=A8=A1=E5=BC=8F?= =?UTF-8?q?=E5=92=8C=E9=A3=8E=E9=80=9F=20fix:=20=E4=BF=AE=E6=AD=A3?= =?UTF-8?q?=E6=A0=87=E5=87=86VAM=E7=9A=84=E9=A3=8E=E9=80=9F=E6=8C=A1?= =?UTF-8?q?=E4=BD=8D=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- custom_components/ds_air/fan.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/custom_components/ds_air/fan.py b/custom_components/ds_air/fan.py index de88563..6eedbc6 100644 --- a/custom_components/ds_air/fan.py +++ b/custom_components/ds_air/fan.py @@ -61,8 +61,10 @@ def __init__(self, vent: Ventilation): self._name = vent.alias self._device_info = vent self._unique_id = vent.unique_id - self._attr_speed_count = 4 - from .ds_air_service.service import Service + if vent.is_small_vam: + self._attr_speed_count = 4 + else: + self._attr_speed_count = 2 Service.register_vent_hook(vent, self._status_change_hook) def _status_change_hook(self, **kwargs): @@ -104,9 +106,8 @@ def should_poll(self) -> bool: @property def supported_features(self) -> int: """Flag supported features.""" - if self._device_info.is_small_vam: - return SMALL_VAM_SUPPORT - return 0 + return SMALL_VAM_SUPPORT + @property def percentage(self) -> int | None: From b957b41105901bbcd2cc41da7f639172078466ac Mon Sep 17 00:00:00 2001 From: lightrabbit Date: Sun, 22 Oct 2023 01:48:41 +0800 Subject: [PATCH 09/16] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81SmallVAM?= =?UTF-8?q?=E7=9A=84=E4=BC=A0=E6=84=9F=E5=99=A8=20feat:=20=E6=8A=8A?= =?UTF-8?q?=E6=96=B0=E9=A3=8E=E7=9B=B8=E5=85=B3=E7=9A=84=E6=97=A5=E5=BF=97?= =?UTF-8?q?=E8=AE=BE=E7=BD=AE=E4=B8=BAwarning=E7=BA=A7=E5=88=AB=EF=BC=8C?= =?UTF-8?q?=E6=96=B9=E4=BE=BFHA=E4=B8=8A=E8=B0=83=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- custom_components/ds_air/const.py | 7 ++ .../ds_air/ds_air_service/dao.py | 16 ++- .../ds_air/ds_air_service/decoder.py | 29 ++++-- .../ds_air/ds_air_service/service.py | 28 ++++-- custom_components/ds_air/fan.py | 19 ++-- custom_components/ds_air/sensor.py | 99 ++++++++++++++++++- 6 files changed, 166 insertions(+), 32 deletions(-) diff --git a/custom_components/ds_air/const.py b/custom_components/ds_air/const.py index 600eb71..dfec259 100644 --- a/custom_components/ds_air/const.py +++ b/custom_components/ds_air/const.py @@ -19,3 +19,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": [TEMP_CELSIUS, None, SensorDeviceClass.TEMPERATURE, 10], + "out_door_temp": [TEMP_CELSIUS, None, SensorDeviceClass.TEMPERATURE, 10], + "out_door_humidity": [PERCENTAGE, None, SensorDeviceClass.HUMIDITY, 1], + "pm25": [CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, None, SensorDeviceClass.PM25, 1], +} \ No newline at end of file diff --git a/custom_components/ds_air/ds_air_service/dao.py b/custom_components/ds_air/ds_air_service/dao.py index 3a23777..a1aa7d5 100644 --- a/custom_components/ds_air/ds_air_service/dao.py +++ b/custom_components/ds_air/ds_air_service/dao.py @@ -98,10 +98,18 @@ class VentilationStatus: def __init__(self, switch: EnumControl.Switch = None, mode: EnumControl.Mode = None, - air_flow: EnumControl.AirFlow = None): - self.switch = switch # type: EnumControl.Switch - self.mode = mode # type: EnumControl.Mode - self.air_flow = air_flow # type: EnumControl.AirFlow + 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): diff --git a/custom_components/ds_air/ds_air_service/decoder.py b/custom_components/ds_air/ds_air_service/decoder.py index 53fd069..69f8e17 100644 --- a/custom_components/ds_air/ds_air_service/decoder.py +++ b/custom_components/ds_air/ds_air_service/decoder.py @@ -934,12 +934,15 @@ def do(self): p = VentilationQueryStatusParam() p.target = self.target p.device = vent - csp = VentilationQueryCompositeSituationParam() - csp.target = self.target - csp.device = vent + from .service import Service Service.send_msg(p) - Service.send_msg(csp) + + if vent.is_small_vam: + csp = VentilationQueryCompositeSituationParam() + csp.target = self.target + csp.device = vent + Service.send_msg(csp) Service.set_ventilations(self._vents) # @property @@ -985,6 +988,8 @@ def __init__(self, cmd_id: int, target: EnumDevice): self.out_door_temp = UNINITIALIZED_VALUE self.out_door_humidity = UNINITIALIZED_VALUE self.pm25 = UNINITIALIZED_VALUE + self.sensors: typing.List[Sensor] = [] + self.humidifierCount: int def load_bytes(self, b): d = Decode(b) @@ -1010,7 +1015,7 @@ def load_bytes(self, b): statusType = d.read1() sensorCount = d.read1() # 关联传感器信息, # 因为本来就能获取到传感器信息,这里就只读取信息,不进行更多的处理 - sensors: typing.List[Sensor] = [] + self.sensors = [] for i in range(0, sensorCount): sensor = Sensor() sensor.sensor_type = d.read1() @@ -1033,13 +1038,21 @@ def load_bytes(self, b): elif statusType == 5 and statusSize == 2: sensor.tvoc = d.read2() elif statusType == 6 and statusSize == 1: #不知道干什么用的数据 - sensor.type1 = d.read2() + sensor.type1 = d.read1() else: d.read(statusSize) statusType = d.read1() - sensors.append(sensor) - humidifierCount = d.read1() # 可能是关联加湿组件信息,手头没有设备,无法调试 + self.sensors.append(sensor) + self.humidifierCount = d.read1() # 可能是关联加湿组件信息,手头没有设备,无法调试 def do(self): + from .service import Service + status = VentilationStatus( + in_door_temp=self.in_door_temp, + out_door_temp=self.out_door_temp, + out_door_humidity=self.out_door_humidity, + pm25=self.pm25, + ) + Service.set_ventilation_status(self.room_id, self.unit_id, status) return diff --git a/custom_components/ds_air/ds_air_service/service.py b/custom_components/ds_air/ds_air_service/service.py index bfcd6cd..5cf139b 100644 --- a/custom_components/ds_air/ds_air_service/service.py +++ b/custom_components/ds_air/ds_air_service/service.py @@ -5,10 +5,10 @@ from threading import Thread, Lock from .ctrl_enum import EnumDevice, EnumControl -from .dao import Room, AirCon, AirConStatus, Ventilation, VentilationStatus, get_device_by_aircon, Sensor, STATUS_ATTR, get_device_by_vent +from .dao import Room, AirCon, AirConStatus, Ventilation, VentilationStatus, get_device_by_aircon, Sensor, STATUS_ATTR, get_device_by_vent, UNINITIALIZED_VALUE from .decoder import decoder, BaseResult from .display import display -from .param import Param, HandShakeParam, HeartbeatParam, AirConControlParam, AirConQueryStatusParam, Sensor2InfoParam, VentilationQueryStatusParam, VentilationControlParam +from .param import Param, HandShakeParam, HeartbeatParam, AirConControlParam, AirConQueryStatusParam, Sensor2InfoParam, VentilationQueryStatusParam, VentilationControlParam, VentilationQueryCompositeSituationParam _LOGGER = logging.getLogger(__name__) @@ -18,6 +18,11 @@ def _log(s: str): for i in s.split('\n'): _LOGGER.debug(i) +def _logError(s: str): + s = str(s) + for i in s.split('\n'): + _LOGGER.error(i) + class SocketClient: def __init__(self, host: str, port: int): @@ -84,7 +89,7 @@ def recv(self) -> (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() @@ -330,8 +335,10 @@ def set_ventilation_status(room: int, unit: int, status: VentilationStatus): else: for i in Service._ventilations: if i.unit_id == unit and i.room_id == room: - i.status = status - i.switch = status.switch + 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 @@ -347,6 +354,11 @@ def poll_status(): 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) @@ -371,8 +383,8 @@ def update_ventilation(room: int, unit: int, **kwargs): try: func(**kwargs) except Exception as e: - _log('vent hook error!!') - _log(str(e)) + _logError('vent hook error!!') + _logError(str(e)) @staticmethod def get_scan_interval(): diff --git a/custom_components/ds_air/fan.py b/custom_components/ds_air/fan.py index 6eedbc6..66dd88d 100644 --- a/custom_components/ds_air/fan.py +++ b/custom_components/ds_air/fan.py @@ -6,7 +6,7 @@ from re import S from typing import Any,Optional, List -from custom_components.ds_air.ds_air_service.display import display +from .ds_air_service.display import display from .ds_air_service.ctrl_enum import _MODE_VENT_NAME_LIST, EnumControl from homeassistant.components.fan import FanEntity, FanEntityFeature @@ -55,8 +55,8 @@ class DsVent(FanEntity): def __init__(self, vent: Ventilation): _log('create ventilation:') - _log(str(vent.__dict__)) - _log(str(vent.switch)) + _log(vent.__dict__) + _log(vent.status) """Initialize the climate device.""" self._name = vent.alias self._device_info = vent @@ -84,6 +84,9 @@ def _status_change_hook(self, **kwargs): 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() @@ -118,14 +121,13 @@ def percentage(self) -> int | None: def set_percentage(self, percentage: int) -> None: vent = self._device_info - status = vent.status new_status = VentilationStatus() air_flow = EnumControl.AirFlow(round(percentage / self.percentage_step)) - status.air_flow = air_flow + vent.status.air_flow = air_flow if air_flow != EnumControl.AirFlow.SUPER_WEAK: new_status.air_flow = air_flow - from .ds_air_service.service import Service 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 @@ -134,7 +136,6 @@ def set_preset_mode(self, preset_mode: str) -> None: mode = EnumControl.get_vent_mode_enum(preset_mode) status.mode = mode new_status.mode = mode - from .ds_air_service.service import Service Service.control_vent(self._device_info, new_status) @property @@ -170,18 +171,16 @@ def turn_on(self, **kwargs: Any) -> None: status.switch = EnumControl.Switch.ON new_status.switch = EnumControl.Switch.ON - from .ds_air_service.service import Service 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.""" - from .ds_air_service.service import Service 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() \ No newline at end of file + self.schedule_update_ha_state() diff --git a/custom_components/ds_air/sensor.py b/custom_components/ds_air/sensor.py index d59ed49..e6b5307 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) @@ -103,3 +109,92 @@ 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] + else: + self._is_available = False + + if not not_update: + self.schedule_update_ha_state() + return True \ No newline at end of file From 9b47bd1b68609d6d5cc5a70d69a8437734c5bfd5 Mon Sep 17 00:00:00 2001 From: lightrabbit Date: Sun, 22 Oct 2023 12:44:11 +0800 Subject: [PATCH 10/16] fix: fix ventilation sensor unavailable when ventilation status changed --- custom_components/ds_air/sensor.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/custom_components/ds_air/sensor.py b/custom_components/ds_air/sensor.py index e6b5307..1040a3b 100644 --- a/custom_components/ds_air/sensor.py +++ b/custom_components/ds_air/sensor.py @@ -192,8 +192,6 @@ def parse_data(self, status: VentilationStatus, not_update: bool = False): self._state = str(value) else: self._state = value / SMALL_VAM_SENSOR_TYPES.get(self._data_key)[3] - else: - self._is_available = False if not not_update: self.schedule_update_ha_state() From 1635ec0798e867983990349ab3784d88e2e18f99 Mon Sep 17 00:00:00 2001 From: lightrabbit Date: Fri, 5 Jan 2024 22:57:30 +0800 Subject: [PATCH 11/16] =?UTF-8?q?fix:=20=E6=88=B7=E5=A4=96=E6=B8=A9?= =?UTF-8?q?=E5=BA=A6=E4=BD=8E=E4=BA=8E0=E5=BA=A6=E6=97=B6=E5=87=BA?= =?UTF-8?q?=E7=8E=B0=E4=B8=8B=E6=BA=A2=E5=87=BA=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- custom_components/ds_air/ds_air_service/decoder.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/custom_components/ds_air/ds_air_service/decoder.py b/custom_components/ds_air/ds_air_service/decoder.py index 69f8e17..4cf2575 100644 --- a/custom_components/ds_air/ds_air_service/decoder.py +++ b/custom_components/ds_air/ds_air_service/decoder.py @@ -130,6 +130,13 @@ def read2(self): self._pos = pos return s + def read_int16(self): + pos = self._pos + s = struct.unpack(' Date: Mon, 27 Jan 2025 11:16:20 +0800 Subject: [PATCH 12/16] fix: Update fan.py to use small VAM on HA 2025.01 Support `TURN_ON` and `TURN_OFF` flags. --- custom_components/ds_air/const.py | 6 +++--- custom_components/ds_air/ds_air_service/ctrl_enum.py | 2 +- custom_components/ds_air/fan.py | 3 +++ custom_components/ds_air/sensor.py | 8 ++++---- 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/custom_components/ds_air/const.py b/custom_components/ds_air/const.py index 79384a8..438f77a 100644 --- a/custom_components/ds_air/const.py +++ b/custom_components/ds_air/const.py @@ -21,8 +21,8 @@ } SMALL_VAM_SENSOR_TYPES = { - "in_door_temp": [TEMP_CELSIUS, None, SensorDeviceClass.TEMPERATURE, 10], - "out_door_temp": [TEMP_CELSIUS, None, SensorDeviceClass.TEMPERATURE, 10], + "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], -} \ No newline at end of file +} 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 4adda81..27d9ee0 100644 --- a/custom_components/ds_air/ds_air_service/ctrl_enum.py +++ b/custom_components/ds_air/ds_air_service/ctrl_enum.py @@ -346,7 +346,7 @@ def get_action_name(idx): @staticmethod def get_mode_enum(name): return Mode(_MODE_NAME_LIST.index(name)) - + @staticmethod def get_vent_mode_name(idx): return _MODE_VENT_NAME_LIST[idx] diff --git a/custom_components/ds_air/fan.py b/custom_components/ds_air/fan.py index 66dd88d..d22e153 100644 --- a/custom_components/ds_air/fan.py +++ b/custom_components/ds_air/fan.py @@ -11,6 +11,7 @@ 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 @@ -31,6 +32,8 @@ LIMITED_SUPPORT = FanEntityFeature.SET_SPEED SMALL_VAM_SUPPORT = FanEntityFeature.SET_SPEED | FanEntityFeature.PRESET_MODE +if (MAJOR_VERSION, MINOR_VERSION) >= (2024, 2): + SMALL_VAM_SUPPORT |= FanEntityFeature.TURN_ON | FanEntityFeature.TURN_OFF _LOGGER = logging.getLogger(__name__) diff --git a/custom_components/ds_air/sensor.py b/custom_components/ds_air/sensor.py index 52464e6..1e63131 100644 --- a/custom_components/ds_air/sensor.py +++ b/custom_components/ds_air/sensor.py @@ -86,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.""" @@ -131,7 +131,7 @@ def name(self): @property def unique_id(self): return "%s_%s" % (self._data_key, self._unique_id) - + @property def device_info(self) -> Optional[DeviceInfo]: return { @@ -172,7 +172,7 @@ def device_class(self): if self._data_key in SMALL_VAM_SENSOR_TYPES else None ) - + @property def state_class(self): """Return the state class of this entity.""" @@ -196,4 +196,4 @@ def parse_data(self, status: VentilationStatus, not_update: bool = False): if not not_update: self.schedule_update_ha_state() - return True \ No newline at end of file + return True From f3b060ac22976488daf3b0318d1ece7f74f4166a Mon Sep 17 00:00:00 2001 From: llcc01 <2417224420@qq.com> Date: Mon, 3 Feb 2025 16:39:23 +0800 Subject: [PATCH 13/16] vam: bump version to 1.3.5 --- custom_components/ds_air/const.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/ds_air/const.py b/custom_components/ds_air/const.py index 79384a8..34130f0 100644 --- a/custom_components/ds_air/const.py +++ b/custom_components/ds_air/const.py @@ -21,8 +21,8 @@ } SMALL_VAM_SENSOR_TYPES = { - "in_door_temp": [TEMP_CELSIUS, None, SensorDeviceClass.TEMPERATURE, 10], - "out_door_temp": [TEMP_CELSIUS, None, SensorDeviceClass.TEMPERATURE, 10], + "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], } \ No newline at end of file From b44c5d891d15a61e312a6594f2a20902ab4d1e8f Mon Sep 17 00:00:00 2001 From: llcc01 <2417224420@qq.com> Date: Mon, 3 Feb 2025 16:40:33 +0800 Subject: [PATCH 14/16] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=A0=87=E5=87=86VAM?= =?UTF-8?q?=E7=9A=84=E9=A3=8E=E9=87=8F=E5=92=8C=E7=94=B5=E6=BA=90=E5=BC=80?= =?UTF-8?q?=E5=85=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ds_air/ds_air_service/ctrl_enum.py | 9 ++++ .../ds_air/ds_air_service/decoder.py | 5 ++- custom_components/ds_air/fan.py | 42 +++++++++++++++---- 3 files changed, 48 insertions(+), 8 deletions(-) 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 4adda81..c25ae54 100644 --- a/custom_components/ds_air/ds_air_service/ctrl_enum.py +++ b/custom_components/ds_air/ds_air_service/ctrl_enum.py @@ -306,6 +306,7 @@ class Mode(IntEnum): _MODE_ACTION_LIST = [HVACAction.COOLING, HVACAction.DRYING, HVACAction.FAN, None, HVACAction.HEATING, HVACAction.DRYING, None, None, HVACAction.PREHEATING, HVACAction.DRYING] _MODE_VENT_NAME_LIST = ["内循环", "热交换", "自动", "防污染", "排异味"] +_MODE_VENT_NAME_LIST2 = ["旁通", "热交换", "自动"] class Switch(IntEnum): OFF = 0 @@ -354,6 +355,14 @@ def get_vent_mode_name(idx): @staticmethod def get_vent_mode_enum(name: str): return Mode(_MODE_VENT_NAME_LIST.index(name)) + + @staticmethod + def get_vent_mode_name2(idx): + return _MODE_VENT_NAME_LIST2[idx] + + @staticmethod + def get_vent_mode_enum2(name: str): + return Mode(_MODE_VENT_NAME_LIST2.index(name)) @staticmethod def get_air_flow_name(idx): diff --git a/custom_components/ds_air/ds_air_service/decoder.py b/custom_components/ds_air/ds_air_service/decoder.py index 4cf2575..5d50cc1 100644 --- a/custom_components/ds_air/ds_air_service/decoder.py +++ b/custom_components/ds_air/ds_air_service/decoder.py @@ -950,7 +950,10 @@ def do(self): csp.target = self.target csp.device = vent Service.send_msg(csp) - Service.set_ventilations(self._vents) + original_vents = Service.get_ventilations() + if original_vents is None: + original_vents = [] + Service.set_ventilations(original_vents + self._vents) # @property # def aircons(self): diff --git a/custom_components/ds_air/fan.py b/custom_components/ds_air/fan.py index 66dd88d..8afc566 100644 --- a/custom_components/ds_air/fan.py +++ b/custom_components/ds_air/fan.py @@ -7,7 +7,7 @@ from typing import Any,Optional, List from .ds_air_service.display import display -from .ds_air_service.ctrl_enum import _MODE_VENT_NAME_LIST, EnumControl +from .ds_air_service.ctrl_enum import _MODE_VENT_NAME_LIST, _MODE_VENT_NAME_LIST2, EnumControl from homeassistant.components.fan import FanEntity, FanEntityFeature from homeassistant.config_entries import ConfigEntry @@ -32,6 +32,8 @@ SMALL_VAM_SUPPORT = FanEntityFeature.SET_SPEED | FanEntityFeature.PRESET_MODE +POWEWR_SUPPORT = FanEntityFeature.TURN_ON | FanEntityFeature.TURN_OFF + _LOGGER = logging.getLogger(__name__) def _log(s: str): @@ -109,7 +111,9 @@ def should_poll(self) -> bool: @property def supported_features(self) -> int: """Flag supported features.""" - return SMALL_VAM_SUPPORT + if self._device_info.is_small_vam: + return SMALL_VAM_SUPPORT + return SMALL_VAM_SUPPORT | POWEWR_SUPPORT @property @@ -117,12 +121,29 @@ def percentage(self) -> int | None: vent = self._device_info if vent.status.air_flow is None: return None - return vent.status.air_flow.value * self.percentage_step + + if vent.is_small_vam: + return vent.status.air_flow.value * self.percentage_step + + if vent.status.air_flow == EnumControl.AirFlow.WEAK: + return 50 + if 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() - air_flow = EnumControl.AirFlow(round(percentage / self.percentage_step)) + + if vent.is_small_vam: + air_flow = EnumControl.AirFlow(round(percentage / self.percentage_step)) + else: + if percentage > 50: + air_flow = EnumControl.AirFlow.STRONG + else: + air_flow = EnumControl.AirFlow.WEAK + vent.status.air_flow = air_flow if air_flow != EnumControl.AirFlow.SUPER_WEAK: new_status.air_flow = air_flow @@ -133,7 +154,10 @@ def set_preset_mode(self, preset_mode: str) -> None: vent = self._device_info status = vent.status new_status = VentilationStatus() - mode = EnumControl.get_vent_mode_enum(preset_mode) + if vent.is_small_vam: + mode = EnumControl.get_vent_mode_enum(preset_mode) + else: + mode = EnumControl.get_vent_mode_enum2(preset_mode) status.mode = mode new_status.mode = mode Service.control_vent(self._device_info, new_status) @@ -142,11 +166,15 @@ def set_preset_mode(self, preset_mode: str) -> None: def preset_mode(self) -> str | None: if self._device_info.status.mode is None: return None - return EnumControl.get_vent_mode_name(self._device_info.status.mode) + if self._device_info.is_small_vam: + return EnumControl.get_vent_mode_name(self._device_info.status.mode) + return EnumControl.get_vent_mode_name2(self._device_info.status.mode) @property def preset_modes(self) -> list[str] | None: - return _MODE_VENT_NAME_LIST + if self._device_info.is_small_vam: + return _MODE_VENT_NAME_LIST + return _MODE_VENT_NAME_LIST2 @property def device_info(self) -> Optional[DeviceInfo]: From 6b8954e623a9b99ab94b84104e7f8979876b5144 Mon Sep 17 00:00:00 2001 From: llcc01 <2417224420@qq.com> Date: Tue, 4 Feb 2025 00:01:54 +0800 Subject: [PATCH 15/16] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=A0=87=E5=87=86VAM?= =?UTF-8?q?=E9=A3=8E=E9=87=8F=E6=8E=A7=E5=88=B6=E9=80=BB=E8=BE=91=E5=B9=B6?= =?UTF-8?q?=E7=A1=AE=E4=BF=9D=E5=BC=80=E5=85=B3=E7=8A=B6=E6=80=81=E6=AD=A3?= =?UTF-8?q?=E7=A1=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- custom_components/ds_air/fan.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/custom_components/ds_air/fan.py b/custom_components/ds_air/fan.py index 8afc566..ccf0d7a 100644 --- a/custom_components/ds_air/fan.py +++ b/custom_components/ds_air/fan.py @@ -141,8 +141,14 @@ def set_percentage(self, percentage: int) -> None: else: if percentage > 50: air_flow = EnumControl.AirFlow.STRONG - else: + 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: From a75dc8235adb454ca00a16258496deefe74dc8ec Mon Sep 17 00:00:00 2001 From: misaka4e21 Date: Sun, 9 Feb 2025 06:03:59 +0800 Subject: [PATCH 16/16] fix: Fix C611 gateways and standards VAMs. Co-authored-by: lightrabbit Co-authored-by: llcc01 --- custom_components/ds_air/__init__.py | 5 +- custom_components/ds_air/climate.py | 2 +- custom_components/ds_air/const.py | 9 ++- .../ds_air/ds_air_service/config.py | 1 + .../ds_air/ds_air_service/ctrl_enum.py | 22 +++---- .../ds_air/ds_air_service/decoder.py | 4 +- custom_components/ds_air/fan.py | 58 +++++++++++-------- 7 files changed, 58 insertions(+), 43 deletions(-) diff --git a/custom_components/ds_air/__init__.py b/custom_components/ds_air/__init__.py index 49be293..e9c6edc 100644 --- a/custom_components/ds_air/__init__.py +++ b/custom_components/ds_air/__init__.py @@ -9,7 +9,7 @@ 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__) @@ -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 438f77a..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], 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 f8cc275..55b47b0 100644 --- a/custom_components/ds_air/ds_air_service/ctrl_enum.py +++ b/custom_components/ds_air/ds_air_service/ctrl_enum.py @@ -305,8 +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 = ["内循环", "热交换", "自动", "防污染", "排异味"] -_MODE_VENT_NAME_LIST2 = ["旁通", "热交换", "自动"] +_MODE_VENT_NAME_LIST_SMALL_VAM = ["内循环", "热交换", "自动", "防污染", "排异味"] +_MODE_VENT_NAME_LIST_STANDARD_VAM = ["旁通", "热交换", "自动"] class Switch(IntEnum): OFF = 0 @@ -349,20 +349,20 @@ def get_mode_enum(name): return Mode(_MODE_NAME_LIST.index(name)) @staticmethod - def get_vent_mode_name(idx): - return _MODE_VENT_NAME_LIST[idx] + def get_vent_mode_name_small_vam(idx): + return _MODE_VENT_NAME_LIST_SMALL_VAM[idx] @staticmethod - def get_vent_mode_enum(name: str): - return Mode(_MODE_VENT_NAME_LIST.index(name)) - + def get_vent_mode_enum_small_vam(name: str): + return Mode(_MODE_VENT_NAME_LIST_SMALL_VAM.index(name)) + @staticmethod - def get_vent_mode_name2(idx): - return _MODE_VENT_NAME_LIST2[idx] + def get_vent_mode_name_standard_vam(idx): + return _MODE_VENT_NAME_LIST_STANDARD_VAM[idx] @staticmethod - def get_vent_mode_enum2(name: str): - return Mode(_MODE_VENT_NAME_LIST2.index(name)) + 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): diff --git a/custom_components/ds_air/ds_air_service/decoder.py b/custom_components/ds_air/ds_air_service/decoder.py index 5d50cc1..6ccdc2b 100644 --- a/custom_components/ds_air/ds_air_service/decoder.py +++ b/custom_components/ds_air/ds_air_service/decoder.py @@ -613,7 +613,7 @@ def load_bytes(self, b): self._time = d.read_utf(14) def do(self): - if Config.is_new_version and Config.is_c611: + if Config.is_d611: p = GetRoomInfoParam(EnumCmdType.SYS_GET_ROOM_INFO_V1) else: p = GetRoomInfoParam(EnumCmdType.SYS_GET_ROOM_INFO) @@ -715,7 +715,7 @@ def load_bytes(self, b): self.mode = EnumControl.Mode(d.read1()) if flag >> 2 & 1: self.air_flow = EnumControl.AirFlow(d.read1()) - if Config.is_c611: + if Config.is_c611 or Config.is_d611: if flag >> 3 & 1: bt = d.read1() self.hum_allow = bt & 8 == 8 diff --git a/custom_components/ds_air/fan.py b/custom_components/ds_air/fan.py index f9bcfe1..bfac2fa 100644 --- a/custom_components/ds_air/fan.py +++ b/custom_components/ds_air/fan.py @@ -7,7 +7,7 @@ from typing import Any,Optional, List from .ds_air_service.display import display -from .ds_air_service.ctrl_enum import _MODE_VENT_NAME_LIST, _MODE_VENT_NAME_LIST2, EnumControl +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 @@ -31,11 +31,17 @@ ) LIMITED_SUPPORT = FanEntityFeature.SET_SPEED +# TODO: Do Standard VAMs use FULL_SUPPORT or LIMITED_SUPPORT? SMALL_VAM_SUPPORT = FanEntityFeature.SET_SPEED | FanEntityFeature.PRESET_MODE -if (MAJOR_VERSION, MINOR_VERSION) >= (2024, 2): - SMALL_VAM_SUPPORT |= FanEntityFeature.TURN_ON | FanEntityFeature.TURN_OFF -POWEWR_SUPPORT = FanEntityFeature.TURN_ON | FanEntityFeature.TURN_OFF +# 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__) @@ -66,10 +72,12 @@ def __init__(self, vent: Ventilation): 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 = 4 + self._attr_speed_count = len(_MODE_VENT_NAME_LIST_SMALL_VAM) - 1 else: - self._attr_speed_count = 2 + 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): @@ -114,25 +122,23 @@ def should_poll(self) -> bool: @property def supported_features(self) -> int: """Flag supported features.""" - if self._device_info.is_small_vam: - return SMALL_VAM_SUPPORT - return SMALL_VAM_SUPPORT | POWEWR_SUPPORT + # 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 - - if vent.status.air_flow == EnumControl.AirFlow.WEAK: - return 50 - if vent.status.air_flow == EnumControl.AirFlow.STRONG: - return 100 - + 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: @@ -164,26 +170,28 @@ def set_preset_mode(self, preset_mode: str) -> None: status = vent.status new_status = VentilationStatus() if vent.is_small_vam: - mode = EnumControl.get_vent_mode_enum(preset_mode) + mode = EnumControl.get_vent_mode_enum_small_vam(preset_mode) else: - mode = EnumControl.get_vent_mode_enum2(preset_mode) + 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 - if self._device_info.is_small_vam: - return EnumControl.get_vent_mode_name(self._device_info.status.mode) - return EnumControl.get_vent_mode_name2(self._device_info.status.mode) + 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 - return _MODE_VENT_NAME_LIST2 + return _MODE_VENT_NAME_LIST_SMALL_VAM + else: + return _MODE_VENT_NAME_LIST_STANDARD_VAM @property def device_info(self) -> Optional[DeviceInfo]: