From 2c928fa093f7680bcc31b64273a661a269bf48e2 Mon Sep 17 00:00:00 2001 From: Pedro Monteiro Date: Fri, 26 Jun 2026 16:27:41 +0100 Subject: [PATCH 1/7] Bump scan interval to 60 seconds --- custom_components/gree_custom/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/gree_custom/const.py b/custom_components/gree_custom/const.py index a2e3661..7c8e4cd 100755 --- a/custom_components/gree_custom/const.py +++ b/custom_components/gree_custom/const.py @@ -40,7 +40,7 @@ DEFAULT_DISABLE_AVAILABLE_CHECK = False DEFAULT_RESTORE_STATES = True MIN_SCAN_INTERVAL = 5 -DEFAULT_SCAN_INTERVAL = 30 +DEFAULT_SCAN_INTERVAL = 60 DEFAULT_DEVICE_UID = 0 DEFAULT_DEVICE_PORT = 7000 From 122edc3bb1a5dbaf16c64818ffa0c468eb8e6177 Mon Sep 17 00:00:00 2001 From: Pedro Monteiro Date: Fri, 26 Jun 2026 16:33:15 +0100 Subject: [PATCH 2/7] Add gather_prop_dump() This allows to debug the value of all unknown props found in firmware disassembly. Might be useful to debug and find new features. TODO: Hook it to HA --- custom_components/gree_custom/aiogree/api.py | 384 ++++++++++++++++-- .../gree_custom/aiogree/device.py | 54 ++- custom_components/gree_custom/config_flow.py | 2 +- custom_components/gree_custom/const.py | 2 +- custom_components/gree_custom/manifest.json | 2 +- 5 files changed, 403 insertions(+), 41 deletions(-) diff --git a/custom_components/gree_custom/aiogree/api.py b/custom_components/gree_custom/aiogree/api.py index 04c6de8..89ca348 100644 --- a/custom_components/gree_custom/aiogree/api.py +++ b/custom_components/gree_custom/aiogree/api.py @@ -1,12 +1,12 @@ """Contains the API to interface with the Gree device.""" -from enum import Enum, IntEnum, unique +from collections.abc import Callable, Mapping +from dataclasses import dataclass +from enum import IntEnum, StrEnum, unique import json import logging import re -from typing import Any - -from attr import dataclass +from typing import Any, TypeVar from .cipher import CipherBase, EncryptionVersion, get_cipher from .const import DEFAULT_DEVICE_PORT @@ -15,8 +15,11 @@ _LOGGER = logging.getLogger(__name__) +PropT = TypeVar("PropT", bound=StrEnum) +PropValueT = TypeVar("PropValueT") + -class GreeProp(Enum): +class GreeProp(StrEnum): """Enumeration of Gree device properties.""" # HVAC CONTROLS @@ -24,23 +27,25 @@ class GreeProp(Enum): POWER = "Pow" # mode of operation OP_MODE = "Mod" + # fan speed mode FAN_SPEED = "WdSpd" + # the swing mode of the horizontal air blades (available on limited number of devices) + SWING_HORIZONTAL = "SwingLfRig" + # the swing mode of the vertical air blades + SWING_VERTICAL = "SwUpDn" + # target temperature TARGET_TEMPERATURE = "SetTem" # used to distinguish between Fahrenheit values TARGET_TEMPERATURE_BIT = "TemRec" # defines the unit of temperature for the target temperature TARGET_TEMPERATURE_UNIT = "TemUn" - # the swing mode of the horizontal air blades (available on limited number of devices) - SWING_HORIZONTAL = "SwingLfRig" - # the swing mode of the vertical air blades - SWING_VERTICAL = "SwUpDn" + # Quiet mode which slows down the fan to its most quiet speed. Not available in Dry and Fan mode. FEAT_QUIET_MODE = "Quiet" # Turbo mode sets fan speed to the maximum. Fan speed cannot be changed while active and only available in Dry and Cool mode FEAT_TURBO_MODE = "Tur" - # OPTIONAL FEATURES/MODES # controls the state of the fresh air valve (not available on all units) FEAT_FRESH_AIR = "Air" @@ -69,18 +74,299 @@ class GreeProp(Enum): SENSOR_OUTSIDE_TEMPERATURE = "OutEnvTem" # indoor humidity sensor, used to read the current room humidity, if available SENSOR_HUMIDITY = "DwatSen" + # error display. 0 if no error, otherwise error + SENSOR_FAULT = "FaultDisplay" # OTHER _UNKNOWN_HEAT_COOL_TYPE = "HeatCoolType" + # If set to 0 the unit will beep on every command BEEPER = "Buzzer_ON_OFF" # If set to 1 the unit will beep on every command (available on newer firmwares) BEEPER_NEW = "BuzzerCtrl" - # EXPERIMENTAL - # error display. 0 if no error, otherwise error - FAULT = "FaultDisplay" - # MODEL = "ModelType" + +PROP_KEY_TO_ENUM = {prop.value: prop for prop in GreeProp} + + +class AllProps(StrEnum): + """Enumeration of Gree device properties.""" + + POWER = "Pow" + OP_MODE = "Mod" + BEEPER = "Buzzer_ON_OFF" + BEEPER_NEW = "BuzzerCtrl" + + FAN_SPEED = "WdSpd" + SWING_HORIZONTAL = "SwingLfRig" + SWING_VERTICAL = "SwUpDn" + + TARGET_TEMPERATURE = "SetTem" + TARGET_TEMPERATURE_BIT = "TemRec" + TARGET_TEMPERATURE_UNIT = "TemUn" + + FEAT_QUIET_MODE = "Quiet" + FEAT_TURBO_MODE = "Tur" + FEAT_FRESH_AIR = "Air" + FEAT_XFAN = "Blo" + FEAT_HEALTH = "Health" + FEAT_SLEEP_MODE_SWING = "SwhSlp" + FEAT_SLEEP_MODE = "SlpMod" + FEAT_LIGHT = "Lig" + FEAT_SMART_HEAT_8C = "StHt" + FEAT_ENERGY_SAVING = "SvSt" + FEAT_ANTI_DIRECT_BLOW = "AntiDirectBlow" + FEAT_SENSOR_LIGHT = "LigSen" + + SENSOR_TEMPERATURE = "TemSen" + SENSOR_OUTSIDE_TEMPERATURE = "OutEnvTem" + SENSOR_HUMIDITY = "DwatSen" + SENSOR_FAULT = "FaultDisplay" + + _UNKN_HEAT_COOL_TYPE = "HeatCoolType" + _UNKN_MODEL = "ModelType" + _UNKN_ACStupPos = "ACStupPos" + _UNKN_ActiveTime = "ActiveTime" + _UNKN_Add0_1 = "Add0.1" + _UNKN_Add0_5 = "Add0.5" + _UNKN_AirQ = "AirQ" + _UNKN_AllErr = "AllErr" + _UNKN_Antifreeze = "Antifreeze" + _UNKN_AssHt = "AssHt" + _UNKN_AutoClean = "AutoClean" + _UNKN_AutoComnCloud = "AutoComnCloud" + _UNKN_AutoUpdate = "AutoUpdate" + _UNKN_BlkTemCom = "BlkTemCom" + _UNKN_ChildLock = "ChildLock" + _UNKN_CO2 = "CO2" + _UNKN_CO2Level = "CO2Level" + _UNKN_CommErr = "CommErr" + _UNKN_CompressorFqy = "CompressorFqy" + _UNKN_CompressorTem = "CompressorTem" + _UNKN_Coolmod = "Coolmod" + _UNKN_CoolNoise = "CoolNoise" + _UNKN_CoolSvStTemMin = "CoolSvStTemMin" + _UNKN_CpsTem = "CpsTem" + _UNKN_CurTmHor = "CurTmHor" + _UNKN_CurTmMin = "CurTmMin" + _UNKN_Dazzling = "Dazzling" + _UNKN_Defrost = "Defrost" + _UNKN_Dfltr = "Dfltr" + _UNKN_DFPoint = "DFPoint" + _UNKN_DIYGra1PoiAmo = "DIYGra1PoiAmo" + _UNKN_Dmod = "Dmod" + _UNKN_DnPLLRSwing = "DnPLLRSwing" + _UNKN_DnPRLRSwing = "DnPRLRSwing" + _UNKN_DnPUDSwing = "DnPUDSwing" + _UNKN_Dpump = "Dpump" + _UNKN_DsplySt = "DsplySt" + _UNKN_DwatFul = "DwatFul" + _UNKN_Dwet = "Dwet" + _UNKN_Elc1Kwh = "Elc1Kwh" + _UNKN_ElcAllKwhClr = "ElcAllKwhClr" + _UNKN_ElcAllKwhH = "ElcAllKwhH" + _UNKN_ElcAllKwhL = "ElcAllKwhL" + _UNKN_ElcDatDte = "ElcDatDte" + _UNKN_ElcDatHor = "ElcDatHor" + _UNKN_ElcDatMth = "ElcDatMth" + _UNKN_ElcErg = "ElcErg" + _UNKN_ElcGear = "ElcGear" + _UNKN_ElcOnKwh = "ElcOnKwh" + _UNKN_ElcP = "ElcP" + _UNKN_Emod = "Emod" + _UNKN_EnergyFlow = "EnergyFlow" + _UNKN_EnvArea1St = "EnvArea1St" + _UNKN_EnvArea2St = "EnvArea2St" + _UNKN_EnvArea3St = "EnvArea3St" + _UNKN_EnvArea4St = "EnvArea4St" + _UNKN_EnvArea5St = "EnvArea5St" + _UNKN_EnvArea6St = "EnvArea6St" + _UNKN_EnvArea7St = "EnvArea7St" + _UNKN_EnvArea8St = "EnvArea8St" + _UNKN_EnvArea9St = "EnvArea9St" + _UNKN_EnvFun = "EnvFun" + _UNKN_EvapClr = "EvapClr" + _UNKN_FanMod = "FanMod" + _UNKN_FavorMode = "FavorMode" + _UNKN_FbidBloPer = "FbidBloPer" + _UNKN_GasAvail = "GasAvail" + _UNKN_GasLED = "GasLED" + _UNKN_GasMas = "GasMas" + _UNKN_GasMod = "GasMod" + _UNKN_GasN = "GasN" + _UNKN_GetEr = "GetEr" + _UNKN_HabitLearn = "HabitLearn" + _UNKN_HandCtl = "HandCtl" + _UNKN_HasTmr = "HasTmr" + _UNKN_HeatCool = "HeatCool" + _UNKN_HeatNoise = "HeatNoise" + _UNKN_HeatSvStTemMax = "HeatSvStTemMax" + _UNKN_HumiSvStTemMin = "HumiSvStTemMin" + _UNKN_HumSen = "HumSen" + _UNKN_HumSor = "HumSor" + _UNKN_IDUAirQu = "IDUAirQu" + _UNKN_ImageRecovery = "ImageRecovery" + _UNKN_ImgUpdateCol = "ImgUpdateCol" + _UNKN_ImgUpdateFail = "ImgUpdateFail" + _UNKN_ImgUpdateSta = "ImgUpdateSta" + _UNKN_ImgUpdateSucs = "ImgUpdateSucs" + _UNKN_ImgVerSta = "ImgVerSta" + _UNKN_InEvaTem = "InEvaTem" + _UNKN_InHid = "InHid" + _UNKN_InHidDownPer = "InHidDownPer" + _UNKN_InHidSvrVer = "InHidSvrVer" + _UNKN_JFErrorCode = "JFErrorCode" + _UNKN_LedLig = "LedLig" + _UNKN_LTemDry = "LTemDry" + _UNKN_MaeS = "MaeS" + _UNKN_MakeWat = "MakeWat" + _UNKN_MasIDUMod = "MasIDUMod" + _UNKN_MasSub = "MasSub" + _UNKN_MicroSen = "MicroSen" + _UNKN_MidType = "MidType" + _UNKN_ModS = "ModS" + _UNKN_NewTimer = "NewTimer" + _UNKN_NewTimerSet = "NewTimerSet" + _UNKN_NobodySave = "NobodySave" + _UNKN_NoD = "NoD" + _UNKN_NoiseSet = "NoiseSet" + _UNKN_ODUViti = "ODUViti" + _UNKN_OEEPHid = "OEEPHid" + _UNKN_OEEPHidDownPer = "OEEPHidDownPer" + _UNKN_OEEPHidSvrVer = "OEEPHidSvrVer" + _UNKN_PctCle = "PctCle" + _UNKN_PctCleOnTm = "PctCleOnTm" + _UNKN_PctCleSetTm = "PctCleSetTm" + _UNKN_PctRe = "PctRe" + _UNKN_PM2P5 = "PM2P5" + _UNKN_PM2P5Sta = "PM2P5Sta" + _UNKN_PM2P5V = "PM2P5V" + _UNKN_PMVComfort = "PMVComfort" + _UNKN_Purify = "Purify" + _UNKN_RemWarnLig = "RemWarnLig" + _UNKN_ReplaceHEPA = "ReplaceHEPA" + _UNKN_ReportCtrl = "ReportCtrl" + _UNKN_ReportFreq = "ReportFreq" + _UNKN_ReportInterval = "ReportInterval" + _UNKN_RoomHigh = "RoomHigh" + _UNKN_RoomLen = "RoomLen" + _UNKN_RoomWid = "RoomWid" + _UNKN_SaveGuid = "SaveGuid" + _UNKN_Security = "Security" + _UNKN_SecurityMode = "SecurityMode" + _UNKN_Sfog = "Sfog" + _UNKN_Slp1H1 = "Slp1H1" + _UNKN_Slp1H2 = "Slp1H2" + _UNKN_Slp1H3 = "Slp1H3" + _UNKN_Slp1H4 = "Slp1H4" + _UNKN_Slp1H5 = "Slp1H5" + _UNKN_Slp1H6 = "Slp1H6" + _UNKN_Slp1H7 = "Slp1H7" + _UNKN_Slp1H8 = "Slp1H8" + _UNKN_Slp1L1 = "Slp1L1" + _UNKN_Slp1L2 = "Slp1L2" + _UNKN_Slp1L3 = "Slp1L3" + _UNKN_Slp1L4 = "Slp1L4" + _UNKN_Slp1L5 = "Slp1L5" + _UNKN_Slp1L6 = "Slp1L6" + _UNKN_Slp1L7 = "Slp1L7" + _UNKN_Slp1L8 = "Slp1L8" + _UNKN_SmartMod = "SmartMod" + _UNKN_SmartSlpMod = "SmartSlpMod" + _UNKN_SmartSlpModEx = "SmartSlpModEx" + _UNKN_SmartWind = "SmartWind" + _UNKN_Smod = "Smod" + _UNKN_SorErr = "SorErr" + _UNKN_Srst = "Srst" + _UNKN_SrstAF = "SrstAF" + _UNKN_SrstCF = "SrstCF" + _UNKN_SrstPF = "SrstPF" + _UNKN_SrstPP = "SrstPP" + _UNKN_SrstRF = "SrstRF" + _UNKN_StSlp1C = "StSlp1C" + _UNKN_StSlp1CInc = "StSlp1CInc" + _UNKN_StSlp1CSp = "StSlp1CSp" + _UNKN_StSlp1H = "StSlp1H" + _UNKN_StSlp1HInc = "StSlp1HInc" + _UNKN_StSlp1HSp = "StSlp1HSp" + _UNKN_StSlp2C = "StSlp2C" + _UNKN_StSlp2CInc = "StSlp2CInc" + _UNKN_StSlp2CSp = "StSlp2CSp" + _UNKN_StSlp2H = "StSlp2H" + _UNKN_StSlp2HInc = "StSlp2HInc" + _UNKN_StSlp2HSp = "StSlp2HSp" + _UNKN_StSlp3C = "StSlp3C" + _UNKN_StSlp3CInc = "StSlp3CInc" + _UNKN_StSlp3CSp = "StSlp3CSp" + _UNKN_StSlp3H = "StSlp3H" + _UNKN_StSlp3HInc = "StSlp3HInc" + _UNKN_StSlp3HSp = "StSlp3HSp" + _UNKN_StSlp4C = "StSlp4C" + _UNKN_StSlp4CInc = "StSlp4CInc" + _UNKN_StSlp4CSp = "StSlp4CSp" + _UNKN_StSlp4H = "StSlp4H" + _UNKN_StSlp4HInc = "StSlp4HInc" + _UNKN_StSlp4HSp = "StSlp4HSp" + _UNKN_StTmr = "StTmr" + _UNKN_Swash = "Swash" + _UNKN_Swat = "Swat" + _UNKN_SwhDIYGra1 = "SwhDIYGra1" + _UNKN_SwhFreAir = "SwhFreAir" + _UNKN_SwhSw = "SwhSw" + _UNKN_SwhWifi = "SwhWifi" + _UNKN_SwhWifiCo = "SwhWifiCo" + _UNKN_SwhWifiRe = "SwhWifiRe" + _UNKN_TemSor = "TemSor" + _UNKN_TemsSenOut = "TemsSenOut" + _UNKN_TmrLpTms = "TmrLpTms" + _UNKN_TmrOff = "TmrOff" + _UNKN_TmrOffHorLf = "TmrOffHorLf" + _UNKN_TmrOffMinLf = "TmrOffMinLf" + _UNKN_TmrOn = "TmrOn" + _UNKN_TmrOnHorLf = "TmrOnHorLf" + _UNKN_TmrOnMinLf = "TmrOnMinLf" + _UNKN_UDFanPort = "UDFanPort" + _UNKN_UnmanedOffTime = "UnmanedOffTime" + _UNKN_UnmanedShutDown = "UnmanedShutDown" + _UNKN_UvcControl = "UvcControl" + _UNKN_Video = "Video" + _UNKN_VitiGr = "VitiGr" + _UNKN_VOC = "VOC" + _UNKN_VocCtl = "VocCtl" + _UNKN_VocIdiom = "VocIdiom" + _UNKN_VocRole = "VocRole" + _UNKN_VocUpdateCol = "VocUpdateCol" + _UNKN_VocUpdateRes = "VocUpdateRes" + _UNKN_VocUpdateSta = "VocUpdateSta" + _UNKN_VocVerSta = "VocVerSta" + _UNKN_WatErr = "WatErr" + _UNKN_WatTmp = "WatTmp" + _UNKN_Werr = "Werr" + _UNKN_Wet = "Wet" + _UNKN_Wmod = "Wmod" + _UNKN_WschOff = "WschOff" + _UNKN_WschOffMin = "WschOffMin" + _UNKN_WschOn = "WschOn" + _UNKN_WschOnMin = "WschOnMin" + _UNKN_WsenNub = "WsenNub" + _UNKN_WsenTmpH = "WsenTmpH" + _UNKN_WsenTmpL = "WsenTmpL" + _UNKN_WsenTmpM = "WsenTmpM" + _UNKN_WsetTmp = "WsetTmp" + _UNKN_WstpH = "WstpH" + _UNKN_WstpSv = "WstpSv" + _UNKN_Wtmr1 = "Wtmr1" + _UNKN_Wtmr1Min = "Wtmr1Min" + _UNKN_Wtmr2 = "Wtmr2" + _UNKN_Wtmr2Min = "Wtmr2Min" + _UNKN_Wtmr3 = "Wtmr3" + _UNKN_Wtmr3Min = "Wtmr3Min" + # # INVALID + # _INV_MafIdf = "MafIdf" + # _INV_DevId = "DevID" + + +ALLPROP_KEY_TO_ENUM = {prop.value: prop for prop in AllProps} @unique @@ -166,9 +452,6 @@ class GreeDiscoveredDevice: subdevices: int -propkey_to_enum = {prop.value: prop for prop in GreeProp} - - async def get_result_pack( json_data: dict, cipher: CipherBase, transport: GreeTransport ) -> dict: @@ -413,18 +696,34 @@ async def gree_get_status( mac_addr_controller: str, mac_addr: str, uid: int, - props: list[GreeProp], + props: list[PropT], + propkey_to_enum: Mapping[str, PropT], cipher: CipherBase, transport: GreeTransport, -) -> tuple[dict[GreeProp, int], list[GreeProp]]: - """Get the status of the device by sending a status request to the device (async). Also returns the props not present.""" + value_parser: Callable[[str], PropValueT] = int, +) -> tuple[dict[PropT, PropValueT], list[PropT]]: + """Get the status of the device by sending a status request to the device (async). Also returns the props not present. - _LOGGER.debug("Trying to get device status") + Gree Protocol is a best-effort key/value response with no guaranteed completeness + + If a invalid prop is requested the response will not have it which is good + However, some "invalid" props are returned in the response with no data, making it impossible to know in a batch where they are + Note: Invalid != Unsupported - status_values_raw: dict[GreeProp, int | None] = {} + Meaning: + + cols = what the device claims it is returning + dat = best-effort values, possibly incomplete + alignment between them is not guaranteed globally + + As such, it is only safe to batch props that are known to work. + """ + + _LOGGER.debug("Trying to get device status") pack = gree_create_status_pack(mac_addr, [prop.value for prop in props]) encrypted_pack, tag = gree_encrypt_pack(pack, cipher) + # WARNING: My device does not respond if the encrypted_pack is more that 1024 bytes json_payload = gree_create_payload( encrypted_pack, "pack", GreeCommand.STATUS, mac_addr_controller, uid, tag ) @@ -438,17 +737,40 @@ async def gree_get_status( except Exception as err: raise GreeProtocolError("Error getting device status") from err - if result["cols"] is None or result["dat"] is None: + cols = result.get("cols") + dat = result.get("dat") + + if cols is None or dat is None: raise GreeProtocolError("No data received while getting device status") - cols = [propkey_to_enum[c] for c in result["cols"] if c in propkey_to_enum] - values = [int(x) if x != "" else None for x in result["dat"]] - status_values_raw = dict(zip(cols, values, strict=True)) + if len(cols) != len(dat): + if len(cols) == 1: + # if there is a single prop without value, add to missing + return {}, [cols] + + raise GreeProtocolError(f"Malformed response: cols={len(cols)} dat={len(dat)}") + + status_values: dict[PropT, PropValueT] = {} + returned_props: set[PropT] = set() + + for key, raw in zip(cols, dat, strict=True): + if key not in propkey_to_enum: + _LOGGER.debug("Ignoring unknown property key: %s", key) + continue + + prop = propkey_to_enum[key] + returned_props.add(prop) + + try: + status_values[prop] = value_parser(raw) + except Exception: + _LOGGER.exception("Failed to parse %s=%r. Skipping", prop, raw) + + invalid_props = [p for p in props if p not in returned_props] - status_values = {k: v for k, v in status_values_raw.items() if v is not None} _LOGGER.debug("Device status values: %s", status_values) - return status_values, [p for p in props if p not in status_values] + return status_values, invalid_props async def gree_set_status( @@ -483,7 +805,7 @@ async def gree_set_status( f"Error setting device status, response code: {result['r']}" ) - options_set = [propkey_to_enum[c] for c in result["opt"] if c in propkey_to_enum] + options_set = [PROP_KEY_TO_ENUM[c] for c in result["opt"] if c in PROP_KEY_TO_ENUM] if options_set is None or len(options_set) == 0: raise GreeProtocolError("No options were set, something went wrong") @@ -515,7 +837,7 @@ async def gree_set_status( async def gree_get_device_info( transport: GreeTransport, cipher: CipherBase | None = None -) -> dict[str, str | None]: +) -> dict[str, str | dict | None]: """Tries to retrive the device info.""" data: dict = await get_result_pack( @@ -526,7 +848,7 @@ async def gree_get_device_info( _LOGGER.debug("Got device info: %s", data) - info: dict[str, str | None] = {} + info: dict[str, str | dict | None] = {} info["raw"] = data info["firmware_version"], info["firmware_code"] = extract_version(data) info["mac"] = data.get("mac", "") diff --git a/custom_components/gree_custom/aiogree/device.py b/custom_components/gree_custom/aiogree/device.py index 205af6a..b5c203e 100755 --- a/custom_components/gree_custom/aiogree/device.py +++ b/custom_components/gree_custom/aiogree/device.py @@ -1,9 +1,13 @@ """Contains the API to interface with the Gree device.""" +from itertools import islice import logging from typing import Any from .api import ( + ALLPROP_KEY_TO_ENUM, + PROP_KEY_TO_ENUM, + AllProps, EncryptionVersion, FanSpeed, GreeDiscoveredDevice, @@ -33,6 +37,13 @@ _LOGGER = logging.getLogger(__name__) +def chunked(iterable, size): + """Creates chunks of data.""" + it = iter(iterable) + while chunk := list(islice(it, size)): + yield chunk + + class GreeDevice: """Representation of a Gree device.""" @@ -151,7 +162,7 @@ async def bind_device(self) -> bool: return True - async def fetch_device_info(self, cipher: CipherBase = None): + async def fetch_device_info(self, cipher: CipherBase | None = None): """Updates the device info fields.""" try: self._raw_info = await gree_get_device_info( @@ -246,6 +257,7 @@ async def fetch_device_status(self): self._mac_addr, self._uid, self._props_to_update, + PROP_KEY_TO_ENUM, self._cipher, self._transport, ) @@ -321,12 +333,10 @@ def _set_device_status(self, props: dict[GreeProp, int]) -> None: """Sets a new local device status. Use 'update_device_status' to update the device.""" self._new_raw_state.update(props) - def _bool_from_raw_state( - self, prop: GreeProp, default: int | None = 0 - ) -> bool | None: + def _bool_from_raw_state(self, prop: GreeProp, default: int | None = 0) -> bool: prop_value: int | None = self._get_prop_raw(prop, default) - return None if prop_value is None else bool(prop_value) + return bool(prop_value) def _remove_unsupported_props(self): """Remove unsupported properties from the list to update.""" @@ -444,6 +454,36 @@ def gather_diagnostics(self) -> dict[str, Any]: return data + async def gather_prop_dump( + self, request_batch: int = 50 + ) -> tuple[dict[AllProps, str], list[AllProps]]: + """Dumps all possible props to the log.""" + props_to_update: list[AllProps] = list(AllProps) + combined_state: dict[AllProps, str] = {} + combined_missing: list[AllProps] = [] + + assert self._cipher is not None + + for props_chunk in chunked(props_to_update, request_batch): + state, missing = await gree_get_status( + self._mac_addr_controller, + self._mac_addr, + self._uid, + props_chunk, + ALLPROP_KEY_TO_ENUM, + self._cipher, + self._transport, + str, + ) + combined_state.update(state) + if len(missing) != 0: + combined_missing.extend(missing) + + _LOGGER.info("%s STATE: %s", self._mac_addr, combined_state) + _LOGGER.info("%s MISSING_PROPS: %s", self._mac_addr, combined_missing) + + return combined_state, combined_missing + def supports_property(self, property: GreeProp) -> bool: """Returns True if the device endpoint supports the property.""" # We consider a property as unsupported if it is not present in the raw state list @@ -512,9 +552,9 @@ def is_bound(self) -> bool: return self._is_bound @property - def has_hvac_error(self) -> bool | None: + def has_hvac_error(self) -> bool: """Return if there is an error with the device.""" - return self._bool_from_raw_state(GreeProp.FAULT, None) + return self._bool_from_raw_state(GreeProp.SENSOR_FAULT) @property def beeper(self) -> bool: diff --git a/custom_components/gree_custom/config_flow.py b/custom_components/gree_custom/config_flow.py index aee846f..8501fe7 100644 --- a/custom_components/gree_custom/config_flow.py +++ b/custom_components/gree_custom/config_flow.py @@ -301,7 +301,7 @@ def build_options_schema( valid_features.append(GATTR_ANTI_DIRECT_BLOW) if device.supports_property(GreeProp.FEAT_ENERGY_SAVING): valid_features.append(GATTR_FEAT_ENERGY_SAVING) - if device.supports_property(GreeProp.FAULT): + if device.supports_property(GreeProp.SENSOR_FAULT): valid_features.append(GATTR_FAULTS) schema.update( diff --git a/custom_components/gree_custom/const.py b/custom_components/gree_custom/const.py index 7c8e4cd..f939fcf 100755 --- a/custom_components/gree_custom/const.py +++ b/custom_components/gree_custom/const.py @@ -99,7 +99,7 @@ GATTR_ANTI_DIRECT_BLOW: GreeProp.FEAT_ANTI_DIRECT_BLOW, GATTR_FEAT_ENERGY_SAVING: GreeProp.FEAT_ENERGY_SAVING, GATTR_FEAT_LIGHT: GreeProp.FEAT_LIGHT, - GATTR_FAULTS: GreeProp.FAULT, + GATTR_FAULTS: GreeProp.SENSOR_FAULT, } # HVAC modes - these come from Home Assistant and are standard diff --git a/custom_components/gree_custom/manifest.json b/custom_components/gree_custom/manifest.json index 3a01925..3d5c0ac 100755 --- a/custom_components/gree_custom/manifest.json +++ b/custom_components/gree_custom/manifest.json @@ -9,5 +9,5 @@ "iot_class": "local_polling", "issue_tracker": "https://github.com/RobHofmann/HomeAssistant-GreeClimateComponent/issues", "requirements": ["pycryptodome", "asyncio_dgram"], - "version": "4.0.0-alpha.99" + "version": "4.0.0-alpha.100" } From 988aec162cce75e999589900cbf818b9fad79588 Mon Sep 17 00:00:00 2001 From: Pedro Monteiro Date: Fri, 26 Jun 2026 16:38:58 +0100 Subject: [PATCH 3/7] Update README from master --- README.md | 10 +++++++--- supported-devices.md | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 0c106cf..93acd02 100644 --- a/README.md +++ b/README.md @@ -3,12 +3,16 @@ # HomeAssistant-GreeClimateComponent -Custom Gree integration for Home Assistant written in Python 3. Controls ACs supporting the Gree UDP protocol. +Custom Gree integration for Home Assistant written in Python 3. This integration connects directly to your HVAC devices via their IP address on the local network, unlike the official mobile app, which establishes a direct connection only during initial setup and subsequently operates through Gree’s servers. -> [!NOTE] -> This integration only supports the Gree UDP protocol. If you have a newer firmware/device that only communicates using the new MQTT protocol, this integration will not work. +**This integration only supports the Gree UDP protocol. If you have a newer firmware/device that only communicates using the new MQTT protocol, this integration will not work.** + +> [!IMPORTANT] +> Due to the many issues being created revolving "TimeOut"/"Cannot connect" errors, I will be closing these. Feel free to make a PR fixing your TimeOut/Cannot connect error. +> +> More information on the "why" can be found here: https://github.com/RobHofmann/HomeAssistant-GreeClimateComponent/issues/405#issuecomment-4300110823 For a comprehensive list of tested devices, see [Supported Devices](supported-devices.md). diff --git a/supported-devices.md b/supported-devices.md index 51f8d8f..3380cea 100644 --- a/supported-devices.md +++ b/supported-devices.md @@ -82,4 +82,4 @@ This helps other users find compatible devices and improves the integration's do - **Encryption Version 1**: Older devices, typically uses ECB encryption - **Encryption Version 2**: Newer devices, typically uses GCM encryption - Most devices require encryption version 2, but some older models use encryption version 1 -- If you're unsure, try encryption version 2 first, then fall back to encryption version 1 if connection fails +- If you're unsure, try encryption version 2 first, then fall back to encryption version 1 if connection fails \ No newline at end of file From bad612d6074d961a08bc46345fa820a36de6b90c Mon Sep 17 00:00:00 2001 From: Pedro Monteiro Date: Fri, 26 Jun 2026 16:40:50 +0100 Subject: [PATCH 4/7] Bump manua-configuration scan interval --- manual-configuration.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manual-configuration.yaml b/manual-configuration.yaml index 3d5ca90..110b4a7 100644 --- a/manual-configuration.yaml +++ b/manual-configuration.yaml @@ -83,7 +83,7 @@ gree_custom: external_temperature_sensor: "None" # Sets a given temperature sensor as the sensor for the AC | str (Entity ID) | default = "None" external_humidity_sensor: "None" # Sets a given humidity sensor as the sensor for the AC | str (Entity ID) | default = "None" restore_states: true # Wether to restore the last HA state to device when HA starts | bool | default = true - scan_interval: 30 # Device polling rate | int > 5 | default = 30 + scan_interval: 60 # Device polling rate | int > 5 | default = 60 # Example for multiple AC units: From ab9aadd59627f64b9306b4face9d3c5c4e9a7479 Mon Sep 17 00:00:00 2001 From: Pedro Monteiro Date: Fri, 26 Jun 2026 17:27:18 +0100 Subject: [PATCH 5/7] Reuse UDP transport and improve async request handling Try to fix connection problems by reusing connection streams with the device while possible: - Reuse a single asyncio_dgram client instead of reconnecting per request - Remove unnecessary recv task creation and await recv() directly - Reset the transport after timeouts or communication errors - Serialize requests with a lock to avoid concurrent send/recv races --- .../gree_custom/aiogree/transport.py | 95 +++++++++++-------- custom_components/gree_custom/const.py | 2 +- 2 files changed, 57 insertions(+), 40 deletions(-) diff --git a/custom_components/gree_custom/aiogree/transport.py b/custom_components/gree_custom/aiogree/transport.py index 9fadd62..5f155e1 100644 --- a/custom_components/gree_custom/aiogree/transport.py +++ b/custom_components/gree_custom/aiogree/transport.py @@ -23,57 +23,74 @@ def __init__( self.max_retries = max_retries self.timeout = timeout + self._stream: asyncio_dgram.DatagramClient | None = None + self._lock: asyncio.Lock = asyncio.Lock() + + async def _get_stream(self) -> asyncio_dgram.DatagramClient: + """Create stream once and reuse it while possible.""" + if self._stream is None: + _LOGGER.debug("Creating stream for %s", self.ip_addr) + self._stream = await asyncio_dgram.connect((self.ip_addr, self.port)) + + return self._stream + + async def _reset_stream(self) -> None: + """Safely reset UDP stream if it gets into a bad state.""" + _LOGGER.debug("Reseting stream for %s", self.ip_addr) + if self._stream is not None: + try: + self._stream.close() + except Exception: + _LOGGER.exception("Could not close stream") + self._stream = None + async def udp_request( self, data: bytes, ) -> bytes: """Send a payload data to the device and reads the response.""" - last_error: Exception = None + last_error: Exception | None = None - for attempt in range(self.max_retries): - stream: asyncio_dgram.DatagramClient | None = None - - try: - stream = await asyncio_dgram.connect((self.ip_addr, self.port)) - - await stream.send(data) - - recv_task = asyncio.create_task(stream.recv()) + async with self._lock: # prevents concurrent recv/send corruption + for attempt in range(self.max_retries): + stream: asyncio_dgram.DatagramClient | None = None try: - received_data, _ = await asyncio.wait_for(recv_task, self.timeout) - except TimeoutError: - recv_task.cancel() - raise + stream = await self._get_stream() + + await stream.send(data) + + received_data, _ = await asyncio.wait_for( + stream.recv(), timeout=self.timeout + ) + + except TimeoutError as err: + last_error = err + _LOGGER.warning( + "Error communicating with %s. Attempt %d/%d", + self.ip_addr, + attempt + 1, + self.max_retries, + ) + await self._reset_stream() + + except Exception as err: # noqa: BLE001 + last_error = err + _LOGGER.warning( + "Error communicating with %s. Attempt %d/%d", + self.ip_addr, + attempt + 1, + self.max_retries, + ) + await self._reset_stream() + else: return received_data - except Exception as err1: # noqa: BLE001 - _LOGGER.warning( - "Error communicating with %s. Attempt %d/%d", - self.ip_addr, - attempt + 1, - self.max_retries, - ) - last_error = err1 - - finally: - if stream: - try: - stream.close() - except Exception as err2: # noqa: BLE001 - _LOGGER.warning( - "Error communicating with %s. Attempt %d/%d", - self.ip_addr, - attempt + 1, - self.max_retries, - ) - last_error = err2 - - # Apply backoff before retrying - if attempt < self.max_retries - 1: - await asyncio.sleep(0.5 + attempt * 0.3) # 0.5s, 0.8s, 1.1s, ... + # Apply backoff before retrying + if attempt < self.max_retries - 1: + await asyncio.sleep(0.5 + attempt * 0.3) # 0.5s, 0.8s, 1.1s, ... raise GreeConnectionError( f"Failed to communicate with device '{self.ip_addr}:{self.port}' after {self.max_retries} attempts" diff --git a/custom_components/gree_custom/const.py b/custom_components/gree_custom/const.py index f939fcf..b3293e5 100755 --- a/custom_components/gree_custom/const.py +++ b/custom_components/gree_custom/const.py @@ -45,7 +45,7 @@ DEFAULT_DEVICE_UID = 0 DEFAULT_DEVICE_PORT = 7000 DEFAULT_CONNECTION_MAX_ATTEMPTS = 3 -DEFAULT_CONNECTION_TIMEOUT = 5 +DEFAULT_CONNECTION_TIMEOUT = 10 DEFAULT_DISCOVERY_TIMEOUT = 5 MAX_UNICAST_SCAN_HOSTS = 65536 From 488dba71b2c4bdc6090d8026a6eb407658479ca1 Mon Sep 17 00:00:00 2001 From: Pedro Monteiro Date: Mon, 29 Jun 2026 11:33:25 +0100 Subject: [PATCH 6/7] Revert "Add gather_prop_dump()" This reverts commit 122edc3bb1a5dbaf16c64818ffa0c468eb8e6177. --- custom_components/gree_custom/aiogree/api.py | 384 ++---------------- .../gree_custom/aiogree/device.py | 50 +-- custom_components/gree_custom/config_flow.py | 2 +- custom_components/gree_custom/const.py | 2 +- custom_components/gree_custom/manifest.json | 2 +- 5 files changed, 38 insertions(+), 402 deletions(-) diff --git a/custom_components/gree_custom/aiogree/api.py b/custom_components/gree_custom/aiogree/api.py index 89ca348..04c6de8 100644 --- a/custom_components/gree_custom/aiogree/api.py +++ b/custom_components/gree_custom/aiogree/api.py @@ -1,12 +1,12 @@ """Contains the API to interface with the Gree device.""" -from collections.abc import Callable, Mapping -from dataclasses import dataclass -from enum import IntEnum, StrEnum, unique +from enum import Enum, IntEnum, unique import json import logging import re -from typing import Any, TypeVar +from typing import Any + +from attr import dataclass from .cipher import CipherBase, EncryptionVersion, get_cipher from .const import DEFAULT_DEVICE_PORT @@ -15,11 +15,8 @@ _LOGGER = logging.getLogger(__name__) -PropT = TypeVar("PropT", bound=StrEnum) -PropValueT = TypeVar("PropValueT") - -class GreeProp(StrEnum): +class GreeProp(Enum): """Enumeration of Gree device properties.""" # HVAC CONTROLS @@ -27,25 +24,23 @@ class GreeProp(StrEnum): POWER = "Pow" # mode of operation OP_MODE = "Mod" - # fan speed mode FAN_SPEED = "WdSpd" - # the swing mode of the horizontal air blades (available on limited number of devices) - SWING_HORIZONTAL = "SwingLfRig" - # the swing mode of the vertical air blades - SWING_VERTICAL = "SwUpDn" - # target temperature TARGET_TEMPERATURE = "SetTem" # used to distinguish between Fahrenheit values TARGET_TEMPERATURE_BIT = "TemRec" # defines the unit of temperature for the target temperature TARGET_TEMPERATURE_UNIT = "TemUn" - + # the swing mode of the horizontal air blades (available on limited number of devices) + SWING_HORIZONTAL = "SwingLfRig" + # the swing mode of the vertical air blades + SWING_VERTICAL = "SwUpDn" # Quiet mode which slows down the fan to its most quiet speed. Not available in Dry and Fan mode. FEAT_QUIET_MODE = "Quiet" # Turbo mode sets fan speed to the maximum. Fan speed cannot be changed while active and only available in Dry and Cool mode FEAT_TURBO_MODE = "Tur" + # OPTIONAL FEATURES/MODES # controls the state of the fresh air valve (not available on all units) FEAT_FRESH_AIR = "Air" @@ -74,299 +69,18 @@ class GreeProp(StrEnum): SENSOR_OUTSIDE_TEMPERATURE = "OutEnvTem" # indoor humidity sensor, used to read the current room humidity, if available SENSOR_HUMIDITY = "DwatSen" - # error display. 0 if no error, otherwise error - SENSOR_FAULT = "FaultDisplay" # OTHER _UNKNOWN_HEAT_COOL_TYPE = "HeatCoolType" - # If set to 0 the unit will beep on every command BEEPER = "Buzzer_ON_OFF" # If set to 1 the unit will beep on every command (available on newer firmwares) BEEPER_NEW = "BuzzerCtrl" - -PROP_KEY_TO_ENUM = {prop.value: prop for prop in GreeProp} - - -class AllProps(StrEnum): - """Enumeration of Gree device properties.""" - - POWER = "Pow" - OP_MODE = "Mod" - BEEPER = "Buzzer_ON_OFF" - BEEPER_NEW = "BuzzerCtrl" - - FAN_SPEED = "WdSpd" - SWING_HORIZONTAL = "SwingLfRig" - SWING_VERTICAL = "SwUpDn" - - TARGET_TEMPERATURE = "SetTem" - TARGET_TEMPERATURE_BIT = "TemRec" - TARGET_TEMPERATURE_UNIT = "TemUn" - - FEAT_QUIET_MODE = "Quiet" - FEAT_TURBO_MODE = "Tur" - FEAT_FRESH_AIR = "Air" - FEAT_XFAN = "Blo" - FEAT_HEALTH = "Health" - FEAT_SLEEP_MODE_SWING = "SwhSlp" - FEAT_SLEEP_MODE = "SlpMod" - FEAT_LIGHT = "Lig" - FEAT_SMART_HEAT_8C = "StHt" - FEAT_ENERGY_SAVING = "SvSt" - FEAT_ANTI_DIRECT_BLOW = "AntiDirectBlow" - FEAT_SENSOR_LIGHT = "LigSen" - - SENSOR_TEMPERATURE = "TemSen" - SENSOR_OUTSIDE_TEMPERATURE = "OutEnvTem" - SENSOR_HUMIDITY = "DwatSen" - SENSOR_FAULT = "FaultDisplay" - - _UNKN_HEAT_COOL_TYPE = "HeatCoolType" - _UNKN_MODEL = "ModelType" - _UNKN_ACStupPos = "ACStupPos" - _UNKN_ActiveTime = "ActiveTime" - _UNKN_Add0_1 = "Add0.1" - _UNKN_Add0_5 = "Add0.5" - _UNKN_AirQ = "AirQ" - _UNKN_AllErr = "AllErr" - _UNKN_Antifreeze = "Antifreeze" - _UNKN_AssHt = "AssHt" - _UNKN_AutoClean = "AutoClean" - _UNKN_AutoComnCloud = "AutoComnCloud" - _UNKN_AutoUpdate = "AutoUpdate" - _UNKN_BlkTemCom = "BlkTemCom" - _UNKN_ChildLock = "ChildLock" - _UNKN_CO2 = "CO2" - _UNKN_CO2Level = "CO2Level" - _UNKN_CommErr = "CommErr" - _UNKN_CompressorFqy = "CompressorFqy" - _UNKN_CompressorTem = "CompressorTem" - _UNKN_Coolmod = "Coolmod" - _UNKN_CoolNoise = "CoolNoise" - _UNKN_CoolSvStTemMin = "CoolSvStTemMin" - _UNKN_CpsTem = "CpsTem" - _UNKN_CurTmHor = "CurTmHor" - _UNKN_CurTmMin = "CurTmMin" - _UNKN_Dazzling = "Dazzling" - _UNKN_Defrost = "Defrost" - _UNKN_Dfltr = "Dfltr" - _UNKN_DFPoint = "DFPoint" - _UNKN_DIYGra1PoiAmo = "DIYGra1PoiAmo" - _UNKN_Dmod = "Dmod" - _UNKN_DnPLLRSwing = "DnPLLRSwing" - _UNKN_DnPRLRSwing = "DnPRLRSwing" - _UNKN_DnPUDSwing = "DnPUDSwing" - _UNKN_Dpump = "Dpump" - _UNKN_DsplySt = "DsplySt" - _UNKN_DwatFul = "DwatFul" - _UNKN_Dwet = "Dwet" - _UNKN_Elc1Kwh = "Elc1Kwh" - _UNKN_ElcAllKwhClr = "ElcAllKwhClr" - _UNKN_ElcAllKwhH = "ElcAllKwhH" - _UNKN_ElcAllKwhL = "ElcAllKwhL" - _UNKN_ElcDatDte = "ElcDatDte" - _UNKN_ElcDatHor = "ElcDatHor" - _UNKN_ElcDatMth = "ElcDatMth" - _UNKN_ElcErg = "ElcErg" - _UNKN_ElcGear = "ElcGear" - _UNKN_ElcOnKwh = "ElcOnKwh" - _UNKN_ElcP = "ElcP" - _UNKN_Emod = "Emod" - _UNKN_EnergyFlow = "EnergyFlow" - _UNKN_EnvArea1St = "EnvArea1St" - _UNKN_EnvArea2St = "EnvArea2St" - _UNKN_EnvArea3St = "EnvArea3St" - _UNKN_EnvArea4St = "EnvArea4St" - _UNKN_EnvArea5St = "EnvArea5St" - _UNKN_EnvArea6St = "EnvArea6St" - _UNKN_EnvArea7St = "EnvArea7St" - _UNKN_EnvArea8St = "EnvArea8St" - _UNKN_EnvArea9St = "EnvArea9St" - _UNKN_EnvFun = "EnvFun" - _UNKN_EvapClr = "EvapClr" - _UNKN_FanMod = "FanMod" - _UNKN_FavorMode = "FavorMode" - _UNKN_FbidBloPer = "FbidBloPer" - _UNKN_GasAvail = "GasAvail" - _UNKN_GasLED = "GasLED" - _UNKN_GasMas = "GasMas" - _UNKN_GasMod = "GasMod" - _UNKN_GasN = "GasN" - _UNKN_GetEr = "GetEr" - _UNKN_HabitLearn = "HabitLearn" - _UNKN_HandCtl = "HandCtl" - _UNKN_HasTmr = "HasTmr" - _UNKN_HeatCool = "HeatCool" - _UNKN_HeatNoise = "HeatNoise" - _UNKN_HeatSvStTemMax = "HeatSvStTemMax" - _UNKN_HumiSvStTemMin = "HumiSvStTemMin" - _UNKN_HumSen = "HumSen" - _UNKN_HumSor = "HumSor" - _UNKN_IDUAirQu = "IDUAirQu" - _UNKN_ImageRecovery = "ImageRecovery" - _UNKN_ImgUpdateCol = "ImgUpdateCol" - _UNKN_ImgUpdateFail = "ImgUpdateFail" - _UNKN_ImgUpdateSta = "ImgUpdateSta" - _UNKN_ImgUpdateSucs = "ImgUpdateSucs" - _UNKN_ImgVerSta = "ImgVerSta" - _UNKN_InEvaTem = "InEvaTem" - _UNKN_InHid = "InHid" - _UNKN_InHidDownPer = "InHidDownPer" - _UNKN_InHidSvrVer = "InHidSvrVer" - _UNKN_JFErrorCode = "JFErrorCode" - _UNKN_LedLig = "LedLig" - _UNKN_LTemDry = "LTemDry" - _UNKN_MaeS = "MaeS" - _UNKN_MakeWat = "MakeWat" - _UNKN_MasIDUMod = "MasIDUMod" - _UNKN_MasSub = "MasSub" - _UNKN_MicroSen = "MicroSen" - _UNKN_MidType = "MidType" - _UNKN_ModS = "ModS" - _UNKN_NewTimer = "NewTimer" - _UNKN_NewTimerSet = "NewTimerSet" - _UNKN_NobodySave = "NobodySave" - _UNKN_NoD = "NoD" - _UNKN_NoiseSet = "NoiseSet" - _UNKN_ODUViti = "ODUViti" - _UNKN_OEEPHid = "OEEPHid" - _UNKN_OEEPHidDownPer = "OEEPHidDownPer" - _UNKN_OEEPHidSvrVer = "OEEPHidSvrVer" - _UNKN_PctCle = "PctCle" - _UNKN_PctCleOnTm = "PctCleOnTm" - _UNKN_PctCleSetTm = "PctCleSetTm" - _UNKN_PctRe = "PctRe" - _UNKN_PM2P5 = "PM2P5" - _UNKN_PM2P5Sta = "PM2P5Sta" - _UNKN_PM2P5V = "PM2P5V" - _UNKN_PMVComfort = "PMVComfort" - _UNKN_Purify = "Purify" - _UNKN_RemWarnLig = "RemWarnLig" - _UNKN_ReplaceHEPA = "ReplaceHEPA" - _UNKN_ReportCtrl = "ReportCtrl" - _UNKN_ReportFreq = "ReportFreq" - _UNKN_ReportInterval = "ReportInterval" - _UNKN_RoomHigh = "RoomHigh" - _UNKN_RoomLen = "RoomLen" - _UNKN_RoomWid = "RoomWid" - _UNKN_SaveGuid = "SaveGuid" - _UNKN_Security = "Security" - _UNKN_SecurityMode = "SecurityMode" - _UNKN_Sfog = "Sfog" - _UNKN_Slp1H1 = "Slp1H1" - _UNKN_Slp1H2 = "Slp1H2" - _UNKN_Slp1H3 = "Slp1H3" - _UNKN_Slp1H4 = "Slp1H4" - _UNKN_Slp1H5 = "Slp1H5" - _UNKN_Slp1H6 = "Slp1H6" - _UNKN_Slp1H7 = "Slp1H7" - _UNKN_Slp1H8 = "Slp1H8" - _UNKN_Slp1L1 = "Slp1L1" - _UNKN_Slp1L2 = "Slp1L2" - _UNKN_Slp1L3 = "Slp1L3" - _UNKN_Slp1L4 = "Slp1L4" - _UNKN_Slp1L5 = "Slp1L5" - _UNKN_Slp1L6 = "Slp1L6" - _UNKN_Slp1L7 = "Slp1L7" - _UNKN_Slp1L8 = "Slp1L8" - _UNKN_SmartMod = "SmartMod" - _UNKN_SmartSlpMod = "SmartSlpMod" - _UNKN_SmartSlpModEx = "SmartSlpModEx" - _UNKN_SmartWind = "SmartWind" - _UNKN_Smod = "Smod" - _UNKN_SorErr = "SorErr" - _UNKN_Srst = "Srst" - _UNKN_SrstAF = "SrstAF" - _UNKN_SrstCF = "SrstCF" - _UNKN_SrstPF = "SrstPF" - _UNKN_SrstPP = "SrstPP" - _UNKN_SrstRF = "SrstRF" - _UNKN_StSlp1C = "StSlp1C" - _UNKN_StSlp1CInc = "StSlp1CInc" - _UNKN_StSlp1CSp = "StSlp1CSp" - _UNKN_StSlp1H = "StSlp1H" - _UNKN_StSlp1HInc = "StSlp1HInc" - _UNKN_StSlp1HSp = "StSlp1HSp" - _UNKN_StSlp2C = "StSlp2C" - _UNKN_StSlp2CInc = "StSlp2CInc" - _UNKN_StSlp2CSp = "StSlp2CSp" - _UNKN_StSlp2H = "StSlp2H" - _UNKN_StSlp2HInc = "StSlp2HInc" - _UNKN_StSlp2HSp = "StSlp2HSp" - _UNKN_StSlp3C = "StSlp3C" - _UNKN_StSlp3CInc = "StSlp3CInc" - _UNKN_StSlp3CSp = "StSlp3CSp" - _UNKN_StSlp3H = "StSlp3H" - _UNKN_StSlp3HInc = "StSlp3HInc" - _UNKN_StSlp3HSp = "StSlp3HSp" - _UNKN_StSlp4C = "StSlp4C" - _UNKN_StSlp4CInc = "StSlp4CInc" - _UNKN_StSlp4CSp = "StSlp4CSp" - _UNKN_StSlp4H = "StSlp4H" - _UNKN_StSlp4HInc = "StSlp4HInc" - _UNKN_StSlp4HSp = "StSlp4HSp" - _UNKN_StTmr = "StTmr" - _UNKN_Swash = "Swash" - _UNKN_Swat = "Swat" - _UNKN_SwhDIYGra1 = "SwhDIYGra1" - _UNKN_SwhFreAir = "SwhFreAir" - _UNKN_SwhSw = "SwhSw" - _UNKN_SwhWifi = "SwhWifi" - _UNKN_SwhWifiCo = "SwhWifiCo" - _UNKN_SwhWifiRe = "SwhWifiRe" - _UNKN_TemSor = "TemSor" - _UNKN_TemsSenOut = "TemsSenOut" - _UNKN_TmrLpTms = "TmrLpTms" - _UNKN_TmrOff = "TmrOff" - _UNKN_TmrOffHorLf = "TmrOffHorLf" - _UNKN_TmrOffMinLf = "TmrOffMinLf" - _UNKN_TmrOn = "TmrOn" - _UNKN_TmrOnHorLf = "TmrOnHorLf" - _UNKN_TmrOnMinLf = "TmrOnMinLf" - _UNKN_UDFanPort = "UDFanPort" - _UNKN_UnmanedOffTime = "UnmanedOffTime" - _UNKN_UnmanedShutDown = "UnmanedShutDown" - _UNKN_UvcControl = "UvcControl" - _UNKN_Video = "Video" - _UNKN_VitiGr = "VitiGr" - _UNKN_VOC = "VOC" - _UNKN_VocCtl = "VocCtl" - _UNKN_VocIdiom = "VocIdiom" - _UNKN_VocRole = "VocRole" - _UNKN_VocUpdateCol = "VocUpdateCol" - _UNKN_VocUpdateRes = "VocUpdateRes" - _UNKN_VocUpdateSta = "VocUpdateSta" - _UNKN_VocVerSta = "VocVerSta" - _UNKN_WatErr = "WatErr" - _UNKN_WatTmp = "WatTmp" - _UNKN_Werr = "Werr" - _UNKN_Wet = "Wet" - _UNKN_Wmod = "Wmod" - _UNKN_WschOff = "WschOff" - _UNKN_WschOffMin = "WschOffMin" - _UNKN_WschOn = "WschOn" - _UNKN_WschOnMin = "WschOnMin" - _UNKN_WsenNub = "WsenNub" - _UNKN_WsenTmpH = "WsenTmpH" - _UNKN_WsenTmpL = "WsenTmpL" - _UNKN_WsenTmpM = "WsenTmpM" - _UNKN_WsetTmp = "WsetTmp" - _UNKN_WstpH = "WstpH" - _UNKN_WstpSv = "WstpSv" - _UNKN_Wtmr1 = "Wtmr1" - _UNKN_Wtmr1Min = "Wtmr1Min" - _UNKN_Wtmr2 = "Wtmr2" - _UNKN_Wtmr2Min = "Wtmr2Min" - _UNKN_Wtmr3 = "Wtmr3" - _UNKN_Wtmr3Min = "Wtmr3Min" - # # INVALID - # _INV_MafIdf = "MafIdf" - # _INV_DevId = "DevID" - - -ALLPROP_KEY_TO_ENUM = {prop.value: prop for prop in AllProps} + # EXPERIMENTAL + # error display. 0 if no error, otherwise error + FAULT = "FaultDisplay" + # MODEL = "ModelType" @unique @@ -452,6 +166,9 @@ class GreeDiscoveredDevice: subdevices: int +propkey_to_enum = {prop.value: prop for prop in GreeProp} + + async def get_result_pack( json_data: dict, cipher: CipherBase, transport: GreeTransport ) -> dict: @@ -696,34 +413,18 @@ async def gree_get_status( mac_addr_controller: str, mac_addr: str, uid: int, - props: list[PropT], - propkey_to_enum: Mapping[str, PropT], + props: list[GreeProp], cipher: CipherBase, transport: GreeTransport, - value_parser: Callable[[str], PropValueT] = int, -) -> tuple[dict[PropT, PropValueT], list[PropT]]: - """Get the status of the device by sending a status request to the device (async). Also returns the props not present. - - Gree Protocol is a best-effort key/value response with no guaranteed completeness - - If a invalid prop is requested the response will not have it which is good - However, some "invalid" props are returned in the response with no data, making it impossible to know in a batch where they are - Note: Invalid != Unsupported - - Meaning: - - cols = what the device claims it is returning - dat = best-effort values, possibly incomplete - alignment between them is not guaranteed globally - - As such, it is only safe to batch props that are known to work. - """ +) -> tuple[dict[GreeProp, int], list[GreeProp]]: + """Get the status of the device by sending a status request to the device (async). Also returns the props not present.""" _LOGGER.debug("Trying to get device status") + status_values_raw: dict[GreeProp, int | None] = {} + pack = gree_create_status_pack(mac_addr, [prop.value for prop in props]) encrypted_pack, tag = gree_encrypt_pack(pack, cipher) - # WARNING: My device does not respond if the encrypted_pack is more that 1024 bytes json_payload = gree_create_payload( encrypted_pack, "pack", GreeCommand.STATUS, mac_addr_controller, uid, tag ) @@ -737,40 +438,17 @@ async def gree_get_status( except Exception as err: raise GreeProtocolError("Error getting device status") from err - cols = result.get("cols") - dat = result.get("dat") - - if cols is None or dat is None: + if result["cols"] is None or result["dat"] is None: raise GreeProtocolError("No data received while getting device status") - if len(cols) != len(dat): - if len(cols) == 1: - # if there is a single prop without value, add to missing - return {}, [cols] - - raise GreeProtocolError(f"Malformed response: cols={len(cols)} dat={len(dat)}") - - status_values: dict[PropT, PropValueT] = {} - returned_props: set[PropT] = set() - - for key, raw in zip(cols, dat, strict=True): - if key not in propkey_to_enum: - _LOGGER.debug("Ignoring unknown property key: %s", key) - continue - - prop = propkey_to_enum[key] - returned_props.add(prop) - - try: - status_values[prop] = value_parser(raw) - except Exception: - _LOGGER.exception("Failed to parse %s=%r. Skipping", prop, raw) - - invalid_props = [p for p in props if p not in returned_props] + cols = [propkey_to_enum[c] for c in result["cols"] if c in propkey_to_enum] + values = [int(x) if x != "" else None for x in result["dat"]] + status_values_raw = dict(zip(cols, values, strict=True)) + status_values = {k: v for k, v in status_values_raw.items() if v is not None} _LOGGER.debug("Device status values: %s", status_values) - return status_values, invalid_props + return status_values, [p for p in props if p not in status_values] async def gree_set_status( @@ -805,7 +483,7 @@ async def gree_set_status( f"Error setting device status, response code: {result['r']}" ) - options_set = [PROP_KEY_TO_ENUM[c] for c in result["opt"] if c in PROP_KEY_TO_ENUM] + options_set = [propkey_to_enum[c] for c in result["opt"] if c in propkey_to_enum] if options_set is None or len(options_set) == 0: raise GreeProtocolError("No options were set, something went wrong") @@ -837,7 +515,7 @@ async def gree_set_status( async def gree_get_device_info( transport: GreeTransport, cipher: CipherBase | None = None -) -> dict[str, str | dict | None]: +) -> dict[str, str | None]: """Tries to retrive the device info.""" data: dict = await get_result_pack( @@ -848,7 +526,7 @@ async def gree_get_device_info( _LOGGER.debug("Got device info: %s", data) - info: dict[str, str | dict | None] = {} + info: dict[str, str | None] = {} info["raw"] = data info["firmware_version"], info["firmware_code"] = extract_version(data) info["mac"] = data.get("mac", "") diff --git a/custom_components/gree_custom/aiogree/device.py b/custom_components/gree_custom/aiogree/device.py index 8a69b83..f0fdfdc 100755 --- a/custom_components/gree_custom/aiogree/device.py +++ b/custom_components/gree_custom/aiogree/device.py @@ -1,13 +1,9 @@ """Contains the API to interface with the Gree device.""" -from itertools import islice import logging from typing import Any from .api import ( - ALLPROP_KEY_TO_ENUM, - PROP_KEY_TO_ENUM, - AllProps, EncryptionVersion, FanSpeed, GreeDiscoveredDevice, @@ -37,13 +33,6 @@ _LOGGER = logging.getLogger(__name__) -def chunked(iterable, size): - """Creates chunks of data.""" - it = iter(iterable) - while chunk := list(islice(it, size)): - yield chunk - - class GreeDevice: """Representation of a Gree device.""" @@ -162,7 +151,7 @@ async def bind_device(self) -> bool: return True - async def fetch_device_info(self, cipher: CipherBase | None = None): + async def fetch_device_info(self, cipher: CipherBase = None): """Updates the device info fields.""" try: self._raw_info = await gree_get_device_info( @@ -257,7 +246,6 @@ async def fetch_device_status(self): self._mac_addr, self._uid, self._props_to_update, - PROP_KEY_TO_ENUM, self._cipher, self._transport, ) @@ -336,7 +324,7 @@ def _set_device_status(self, props: dict[GreeProp, int]) -> None: def _bool_from_raw_state(self, prop: GreeProp, default: int = 0) -> bool: prop_value: int | None = self._get_prop_raw(prop, default) - return bool(prop_value) + return None if prop_value is None else bool(prop_value) def _remove_unsupported_props(self): """Remove unsupported properties from the list to update.""" @@ -454,36 +442,6 @@ def gather_diagnostics(self) -> dict[str, Any]: return data - async def gather_prop_dump( - self, request_batch: int = 50 - ) -> tuple[dict[AllProps, str], list[AllProps]]: - """Dumps all possible props to the log.""" - props_to_update: list[AllProps] = list(AllProps) - combined_state: dict[AllProps, str] = {} - combined_missing: list[AllProps] = [] - - assert self._cipher is not None - - for props_chunk in chunked(props_to_update, request_batch): - state, missing = await gree_get_status( - self._mac_addr_controller, - self._mac_addr, - self._uid, - props_chunk, - ALLPROP_KEY_TO_ENUM, - self._cipher, - self._transport, - str, - ) - combined_state.update(state) - if len(missing) != 0: - combined_missing.extend(missing) - - _LOGGER.info("%s STATE: %s", self._mac_addr, combined_state) - _LOGGER.info("%s MISSING_PROPS: %s", self._mac_addr, combined_missing) - - return combined_state, combined_missing - def supports_property(self, property: GreeProp) -> bool: """Returns True if the device endpoint supports the property.""" # We consider a property as unsupported if it is not present in the raw state list @@ -552,9 +510,9 @@ def is_bound(self) -> bool: return self._is_bound @property - def has_hvac_error(self) -> bool: + def has_hvac_error(self) -> bool | None: """Return if there is an error with the device.""" - return self._bool_from_raw_state(GreeProp.SENSOR_FAULT) + return self._bool_from_raw_state(GreeProp.FAULT, None) @property def beeper(self) -> bool: diff --git a/custom_components/gree_custom/config_flow.py b/custom_components/gree_custom/config_flow.py index 8501fe7..aee846f 100644 --- a/custom_components/gree_custom/config_flow.py +++ b/custom_components/gree_custom/config_flow.py @@ -301,7 +301,7 @@ def build_options_schema( valid_features.append(GATTR_ANTI_DIRECT_BLOW) if device.supports_property(GreeProp.FEAT_ENERGY_SAVING): valid_features.append(GATTR_FEAT_ENERGY_SAVING) - if device.supports_property(GreeProp.SENSOR_FAULT): + if device.supports_property(GreeProp.FAULT): valid_features.append(GATTR_FAULTS) schema.update( diff --git a/custom_components/gree_custom/const.py b/custom_components/gree_custom/const.py index b3293e5..dcd4593 100755 --- a/custom_components/gree_custom/const.py +++ b/custom_components/gree_custom/const.py @@ -99,7 +99,7 @@ GATTR_ANTI_DIRECT_BLOW: GreeProp.FEAT_ANTI_DIRECT_BLOW, GATTR_FEAT_ENERGY_SAVING: GreeProp.FEAT_ENERGY_SAVING, GATTR_FEAT_LIGHT: GreeProp.FEAT_LIGHT, - GATTR_FAULTS: GreeProp.SENSOR_FAULT, + GATTR_FAULTS: GreeProp.FAULT, } # HVAC modes - these come from Home Assistant and are standard diff --git a/custom_components/gree_custom/manifest.json b/custom_components/gree_custom/manifest.json index 3d5c0ac..3a01925 100755 --- a/custom_components/gree_custom/manifest.json +++ b/custom_components/gree_custom/manifest.json @@ -9,5 +9,5 @@ "iot_class": "local_polling", "issue_tracker": "https://github.com/RobHofmann/HomeAssistant-GreeClimateComponent/issues", "requirements": ["pycryptodome", "asyncio_dgram"], - "version": "4.0.0-alpha.100" + "version": "4.0.0-alpha.99" } From 1063ad4ab16e83c7eebadd268e043211ec7f9494 Mon Sep 17 00:00:00 2001 From: Pedro Monteiro Date: Mon, 29 Jun 2026 11:34:45 +0100 Subject: [PATCH 7/7] Bump version --- custom_components/gree_custom/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/gree_custom/manifest.json b/custom_components/gree_custom/manifest.json index 3a01925..3d5c0ac 100755 --- a/custom_components/gree_custom/manifest.json +++ b/custom_components/gree_custom/manifest.json @@ -9,5 +9,5 @@ "iot_class": "local_polling", "issue_tracker": "https://github.com/RobHofmann/HomeAssistant-GreeClimateComponent/issues", "requirements": ["pycryptodome", "asyncio_dgram"], - "version": "4.0.0-alpha.99" + "version": "4.0.0-alpha.100" }