From 9c29f79239b7a459abbd6be7b5731f7310877ef2 Mon Sep 17 00:00:00 2001 From: Radek Piekarz Date: Sun, 13 Sep 2026 14:41:52 +0200 Subject: [PATCH 1/2] Probe optional properties by column presence, add inverter diagnostics The feature probe treated a property as unsupported when its value was falsy. Devices report 0 for plenty of properties they do support -- a stopped compressor, an unlocked child lock, a fault register with no fault -- so the probe result depended on what the unit happened to be doing when Home Assistant started. AntiDirectBlow, for example, reads 2 on one unit and 0 on another of the same model; only the first was detected. Decide instead on whether the device echoes the column back. The firmware omits columns it does not implement from a status response, so the returned column list is exactly the supported subset. GreeGetValues discarded that list, so split the request out into GreeFetchStatus and add GreeGetSupportedValues alongside it. Probing is what keeps the poll list safe: SetAcOptions maps dat[i] positionally onto the requested columns, so polling an unsupported key would shift every following value and push a wrong mode or temperature back to the unit on the next SendStateToAc. Replace the five copy-pasted probe blocks with a table in const.py and one loop. All pending keys go out in a single request -- the reply already classifies them -- falling back to per-key probes if a device rejects the longer column list. Add the properties this makes reachable: sensors CompressorFqy, CompressorTem, InEvaTem, EnvTem, TemsSenOut, PM2P5, AllErr, JFErrorCode binary sensors Dfltr, ReplaceHEPA (new platform) switches ChildLock, Dazzling, UvcControl, AutoClean, NobodySave Writable ones are appended to the SendStateToAc command list; values left unset by an unsupported feature are dropped by the existing filter. CompressorFqy is documented as a system-level reading: on a multi-split every indoor unit reports the shared outdoor unit's frequency, so it must not be summed across units. Verified against four Gree units (fw V3.2.M, module V2.10, encryption v2): all 20 probed keys echo back in one request. --- custom_components/gree/__init__.py | 2 +- custom_components/gree/binary_sensor.py | 81 +++++++ custom_components/gree/climate.py | 229 ++++++++++++-------- custom_components/gree/const.py | 43 ++++ custom_components/gree/sensor.py | 85 +++++++- custom_components/gree/switch.py | 55 +++++ custom_components/gree/translations/en.json | 78 ++++++- custom_components/gree/translations/pl.json | 64 ++++++ 8 files changed, 537 insertions(+), 100 deletions(-) create mode 100644 custom_components/gree/binary_sensor.py diff --git a/custom_components/gree/__init__.py b/custom_components/gree/__init__.py index a5858931..f298759b 100644 --- a/custom_components/gree/__init__.py +++ b/custom_components/gree/__init__.py @@ -41,7 +41,7 @@ OPTION_KEYS, ) -PLATFORMS = [Platform.CLIMATE, Platform.SWITCH, Platform.NUMBER, Platform.SELECT, Platform.SENSOR] +PLATFORMS = [Platform.CLIMATE, Platform.SWITCH, Platform.NUMBER, Platform.SELECT, Platform.SENSOR, Platform.BINARY_SENSOR] _LOGGER = logging.getLogger(__name__) # YAML configuration schema diff --git a/custom_components/gree/binary_sensor.py b/custom_components/gree/binary_sensor.py new file mode 100644 index 00000000..65654fbe --- /dev/null +++ b/custom_components/gree/binary_sensor.py @@ -0,0 +1,81 @@ +"""Support for Gree binary sensors.""" + +from __future__ import annotations + +# Standard library imports +import logging +from dataclasses import dataclass + +# Home Assistant imports +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, + BinarySensorEntityDescription, +) +from homeassistant.helpers.entity import EntityCategory + +# Local imports +from .const import DOMAIN +from .entity import GreeEntity, GreeEntityDescription + +_LOGGER = logging.getLogger(__name__) + + +@dataclass +class GreeBinarySensorEntityDescription(GreeEntityDescription, BinarySensorEntityDescription): + """Describes Gree binary sensor entity.""" + + pass + + +BINARY_SENSORS: tuple[GreeBinarySensorEntityDescription, ...] = ( + GreeBinarySensorEntityDescription( + property_key="filter_alarm", + device_class=BinarySensorDeviceClass.PROBLEM, + entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:air-filter", + value_fn=lambda device: device.filter_alarm, + available_fn=lambda device: device.available and bool(device._has_filter_alarm), + ), + GreeBinarySensorEntityDescription( + property_key="hepa_alarm", + device_class=BinarySensorDeviceClass.PROBLEM, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + icon="mdi:air-purifier", + value_fn=lambda device: device.hepa_alarm, + available_fn=lambda device: device.available and bool(device._has_hepa_alarm), + ), +) + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up Gree binary sensors from a config entry.""" + entry_data = hass.data[DOMAIN][entry.entry_id] + device = entry_data["device"] + + sensors = [ + GreeBinarySensor(hass, entry, description) + for description in BINARY_SENSORS + if description.exists_fn(description, device) + ] + + if sensors: + async_add_entities(sensors) + _LOGGER.info(f"Added {len(sensors)} Gree binary sensors") + + +class GreeBinarySensor(GreeEntity, BinarySensorEntity): + """Gree binary sensor entity.""" + + entity_description: GreeBinarySensorEntityDescription + + @property + def is_on(self) -> bool | None: + """Return True when the device reports a problem.""" + return self.entity_description.value_fn(self._device) + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self.entity_description.available_fn(self._device) diff --git a/custom_components/gree/climate.py b/custom_components/gree/climate.py index 539373ef..51a8a478 100644 --- a/custom_components/gree/climate.py +++ b/custom_components/gree/climate.py @@ -43,6 +43,9 @@ MAX_TEMP_F, MODES_MAPPING, TEMSEN_OFFSET, + DIAGNOSTIC_TEMP_OFFSET, + PROBED_PROPS, + CONTROLLABLE_OPTIONAL_PROPS, CONF_HVAC_MODES, CONF_FAN_MODES, CONF_SWING_MODES, @@ -183,11 +186,9 @@ def __init__( # Keep unsub callbacks for deregistering listeners self._listeners: list = [] - self._has_temp_sensor = None - self._has_anti_direct_blow = None - self._has_light_sensor = None - self._has_outside_temp_sensor = None - self._has_room_humidity_sensor = None + # Optional-feature flags: None = not probed yet, True/False = probe result. + for _prop, _flag in PROBED_PROPS: + setattr(self, _flag, None) self._current_temperature = None self._current_anti_direct_blow = None @@ -251,7 +252,8 @@ def __init__( # helper method to determine TemSen offset self._process_temp_sensor = TempOffsetResolver() - async def GreeGetValues(self, propertyNames): + async def GreeFetchStatus(self, propertyNames): + """Send a status request for propertyNames and return the decoded response.""" plaintext = '{"cols":' + simplejson.dumps(propertyNames) + ',"mac":"' + str(self._sub_mac_addr) + '","t":"status"}' if self.encryption_version == 1: cipher = self.CIPHER @@ -260,9 +262,21 @@ async def GreeGetValues(self, propertyNames): pack, tag = EncryptGCM(self._encryption_key, plaintext) jsonPayloadToSend = '{"cid":"app","i":0,"pack":"' + pack + '","t":"pack","tcid":"' + str(self._mac_addr) + '","uid":{}'.format(self._uid) + ',"tag" : "' + tag + '"}' cipher = GetGCMCipher(self._encryption_key) - result = await FetchResult(cipher, self._ip_addr, self._port, jsonPayloadToSend, encryption_version=self.encryption_version) + return await FetchResult(cipher, self._ip_addr, self._port, jsonPayloadToSend, encryption_version=self.encryption_version) + + async def GreeGetValues(self, propertyNames): + result = await self.GreeFetchStatus(propertyNames) return result["dat"][0] if len(result["dat"]) == 1 else result["dat"] + async def GreeGetSupportedValues(self, propertyNames): + """Return {column: value} for the requested properties. + + The device answers with only those columns it actually implements, so the keys of + the returned mapping are exactly the supported subset of propertyNames. + """ + result = await self.GreeFetchStatus(propertyNames) + return dict(zip(result.get("cols", []), result.get("dat", []))) + def SetAcOptions(self, acOptions, newOptionsToOverride, optionValuesToOverride=None): if optionValuesToOverride is not None: # Build a list of key-value pairs for a single log line @@ -283,6 +297,9 @@ def SetAcOptions(self, acOptions, newOptionsToOverride, optionValuesToOverride=N async def SendStateToAc(self): opt_list = ["Pow", "Mod", "SetTem", "WdSpd", "Air", "Blo", "Health", "SwhSlp", "Lig", "SwingLfRig", "SwUpDn", "Quiet", "Tur", "StHt", "TemUn", "HeatCoolType", "TemRec", "SvSt", "SlpMod", "AntiDirectBlow", "LigSen"] + # Writable optional properties. Ones the device never reported stay None in + # _acOptions and are dropped by the filter below. + opt_list += [prop for prop in CONTROLLABLE_OPTIONAL_PROPS if prop not in opt_list] # Collect values from _acOptions p_values = [self._acOptions.get(k) for k in opt_list] @@ -466,93 +483,55 @@ def UpdateHAStateToCurrentACState(self): self.UpdateHAOutsideTemperature() self.UpdateHARoomHumidity() - async def SyncState(self, acOptions={}): - # Fetch current settings from HVAC - _LOGGER.debug(f"{self._name}: Starting device state sync") - - if self._has_temp_sensor is None: - _LOGGER.debug("Attempt to check whether device has an built-in temperature sensor") - try: - temp_sensor = await self.GreeGetValues(["TemSen"]) - except Exception: - _LOGGER.debug("Could not determine whether device has an built-in temperature sensor. Retrying at next update()") - else: - if temp_sensor: - self._has_temp_sensor = True - self._acOptions.update({"TemSen": None}) - self._optionsToFetch.append("TemSen") - _LOGGER.debug("Device has an built-in temperature sensor") - else: - self._has_temp_sensor = False - _LOGGER.debug("Device has no built-in temperature sensor") + async def _ProbeOptionalProps(self): + """Detect which optional properties this device implements, once per property. + + A property counts as supported when the device echoes its name back in the + response's column list. Testing the value instead would misclassify a legitimate + zero -- a stopped compressor, an unlocked child lock -- as an absent feature. + + Every supported key is appended to the poll list. Unsupported keys must stay out + of it: the device omits unknown columns from its reply, and SetAcOptions() pairs + the reply positionally with the requested column list, so one missing column would + shift every value after it. + """ + pending = [(prop, flag) for prop, flag in PROBED_PROPS if getattr(self, flag, None) is None] + if not pending: + return + + # One request covering every pending key: the reply already tells us which of them + # exist. Older firmware that rejects a long or unfamiliar column list falls back to + # probing each key on its own. + try: + supported = set(await self.GreeGetSupportedValues([prop for prop, _ in pending])) + except Exception as e: + _LOGGER.debug(f"{self._name}: Batch feature probe failed ({e}), falling back to individual probes") + supported = set() + for prop, _flag in pending: + try: + if prop in await self.GreeGetSupportedValues([prop]): + supported.add(prop) + except Exception: + _LOGGER.debug(f"{self._name}: Could not probe {prop}. Retrying at next update()") + return - # Check if device has anti direct blow feature - if self._has_anti_direct_blow is None: - _LOGGER.debug("Attempt to check whether device has an anti direct blow feature") - try: - anti_direct_blow = await self.GreeGetValues(["AntiDirectBlow"]) - except Exception: - _LOGGER.debug("Could not determine whether device has an anti direct blow feature. Retrying at next update()") - else: - if anti_direct_blow: - self._has_anti_direct_blow = True - self._acOptions.update({"AntiDirectBlow": None}) - self._optionsToFetch.append("AntiDirectBlow") - _LOGGER.debug("Device has an anti direct blow feature") - else: - self._has_anti_direct_blow = False - _LOGGER.debug("Device has no anti direct blow feature") + for prop, flag in pending: + is_supported = prop in supported + setattr(self, flag, is_supported) + if is_supported: + self._acOptions.update({prop: None}) + self._optionsToFetch.append(prop) - # Check if device has light sensor - if self._has_light_sensor is None: - _LOGGER.debug("Attempt to check whether device has a built-in light sensor") - try: - light_sensor = await self.GreeGetValues(["LigSen"]) - except Exception: - _LOGGER.debug("Could not determine whether device has a built-in light sensor. Retrying at next update()") - else: - if light_sensor: - self._has_light_sensor = True - self._acOptions.update({"LigSen": None}) - self._optionsToFetch.append("LigSen") - _LOGGER.debug("Device has a built-in light sensor") - else: - self._has_light_sensor = False - _LOGGER.debug("Device has no built-in light sensor") + found = sorted(prop for prop, _ in pending if prop in supported) + missing = sorted(prop for prop, _ in pending if prop not in supported) + _LOGGER.debug(f"{self._name}: Optional properties supported: {found or 'none'}") + _LOGGER.debug(f"{self._name}: Optional properties not supported: {missing or 'none'}") - # Check if device has outside temperature sensor - if self._has_outside_temp_sensor is None: - _LOGGER.debug("Attempt to check whether device has an outside temperature sensor") - try: - outside_temp_sensor = await self.GreeGetValues(["OutEnvTem"]) - except Exception: - _LOGGER.debug("Could not determine whether device has an outside temperature sensor. Retrying at next update()") - else: - if outside_temp_sensor: - self._has_outside_temp_sensor = True - self._acOptions.update({"OutEnvTem": None}) - self._optionsToFetch.append("OutEnvTem") - _LOGGER.debug("Device has an outside temperature sensor") - else: - self._has_outside_temp_sensor = False - _LOGGER.debug("Device has no outside temperature sensor") + async def SyncState(self, acOptions={}): + # Fetch current settings from HVAC + _LOGGER.debug(f"{self._name}: Starting device state sync") - # Check if device has room humidity sensor - if self._has_room_humidity_sensor is None: - _LOGGER.debug("Attempt to check whether device has a room humidity sensor") - try: - humidity_sensor = await self.GreeGetValues(["DwatSen"]) - except Exception: - _LOGGER.debug("Could not determine whether device has a room humidity sensor. Retrying at next update()") - else: - if humidity_sensor: - self._has_room_humidity_sensor = True - self._acOptions.update({"DwatSen": None}) - self._optionsToFetch.append("DwatSen") - _LOGGER.debug("Device has a room humidity sensor") - else: - self._has_room_humidity_sensor = False - _LOGGER.debug("Device has no room humidity sensor") + await self._ProbeOptionalProps() optionsToFetch = self._optionsToFetch @@ -780,6 +759,76 @@ def room_humidity(self): return self._current_room_humidity return None + def _diagnostic_value(self, key, flag): + """Return a raw optional property, or None when the device does not report it.""" + if not getattr(self, flag, False): + return None + return self._acOptions.get(key) + + def _diagnostic_temperature(self, key, flag): + """Same as _diagnostic_value but decodes the +40 °C sensor offset.""" + raw = self._diagnostic_value(key, flag) + if raw is None: + return None + return raw - DIAGNOSTIC_TEMP_OFFSET + + @property + def compressor_frequency(self): + """Return the outdoor compressor frequency in Hz. + + On multi-split systems every indoor unit reports the frequency of the shared + outdoor unit, so this value is a property of the system, not of one room. + """ + return self._diagnostic_value("CompressorFqy", "_has_compressor_freq") + + @property + def compressor_temperature(self): + """Return the compressor temperature in °C.""" + return self._diagnostic_temperature("CompressorTem", "_has_compressor_temp") + + @property + def evaporator_temperature(self): + """Return the indoor coil (evaporator) temperature in °C.""" + return self._diagnostic_temperature("InEvaTem", "_has_evaporator_temp") + + @property + def env_temperature(self): + """Return the secondary indoor ambient sensor reading in °C.""" + return self._diagnostic_temperature("EnvTem", "_has_env_temp") + + @property + def outside_temperature_alt(self): + """Return the secondary outdoor sensor reading in °C.""" + return self._diagnostic_temperature("TemsSenOut", "_has_outside_temp_alt") + + @property + def pm25(self): + """Return the PM2.5 reading, or None when the device reports no measurement.""" + value = self._diagnostic_value("PM2P5", "_has_pm25") + return value if value else None + + @property + def error_code(self): + """Return the aggregated fault code (0 means no fault).""" + return self._diagnostic_value("AllErr", "_has_all_err") + + @property + def jf_error_code(self): + """Return the secondary fault code register (0 means no fault).""" + return self._diagnostic_value("JFErrorCode", "_has_jf_error") + + @property + def filter_alarm(self): + """Return True when the device asks for filter cleaning.""" + value = self._diagnostic_value("Dfltr", "_has_filter_alarm") + return None if value is None else bool(value) + + @property + def hepa_alarm(self): + """Return True when the device asks for HEPA replacement.""" + value = self._diagnostic_value("ReplaceHEPA", "_has_hepa_alarm") + return None if value is None else bool(value) + @property def extra_state_attributes(self): """Return additional state attributes.""" diff --git a/custom_components/gree/const.py b/custom_components/gree/const.py index c8d65310..66ad1575 100644 --- a/custom_components/gree/const.py +++ b/custom_components/gree/const.py @@ -25,6 +25,49 @@ TEMSEN_OFFSET = 40 +# Sensor readings that arrive with a +40 °C encoding offset (actual = raw - 40). +DIAGNOSTIC_TEMP_OFFSET = 40 + +# Optional device properties, probed once before they join the polling list. +# +# Probing is mandatory rather than defensive. The firmware silently omits unknown columns +# from a status response, while SetAcOptions() maps dat[i] positionally onto the list of +# columns that was requested. Polling a key the device does not implement therefore shifts +# every following value by one position, and the next SendStateToAc() would push a wrong +# mode or temperature to the unit. +# +# Each entry is (property key, attribute name used as the "device has this" flag). +PROBED_PROPS = ( + ("TemSen", "_has_temp_sensor"), + ("AntiDirectBlow", "_has_anti_direct_blow"), + ("LigSen", "_has_light_sensor"), + ("OutEnvTem", "_has_outside_temp_sensor"), + ("DwatSen", "_has_room_humidity_sensor"), + # Inverter and air-quality diagnostics + ("CompressorFqy", "_has_compressor_freq"), + ("CompressorTem", "_has_compressor_temp"), + ("InEvaTem", "_has_evaporator_temp"), + ("EnvTem", "_has_env_temp"), + ("TemsSenOut", "_has_outside_temp_alt"), + ("PM2P5", "_has_pm25"), + # Fault and maintenance reporting + ("AllErr", "_has_all_err"), + ("JFErrorCode", "_has_jf_error"), + ("Dfltr", "_has_filter_alarm"), + ("ReplaceHEPA", "_has_hepa_alarm"), + # Comfort and cleaning toggles + ("ChildLock", "_has_child_lock"), + ("Dazzling", "_has_dazzling"), + ("UvcControl", "_has_uvc"), + ("AutoClean", "_has_auto_clean"), + ("NobodySave", "_has_nobody_save"), +) + +# Optional properties that are writable, not just readable. They are appended to the +# command sent by SendStateToAc(); values still unset (device lacks the feature) are +# filtered out there, so listing one a device does not implement is harmless. +CONTROLLABLE_OPTIONAL_PROPS = ("ChildLock", "Dazzling", "UvcControl", "AutoClean", "NobodySave") + # HVAC modes - these come from Home Assistant and are standard DEFAULT_HVAC_MODES = ["auto", "cool", "dry", "fan_only", "heat", "off"] diff --git a/custom_components/gree/sensor.py b/custom_components/gree/sensor.py index b9b3d1ac..7176c132 100644 --- a/custom_components/gree/sensor.py +++ b/custom_components/gree/sensor.py @@ -14,7 +14,11 @@ SensorStateClass, ) from homeassistant.const import ( + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, PERCENTAGE, + EntityCategory, + UnitOfFrequency, + UnitOfTemperature, ) @@ -50,6 +54,83 @@ class GreeSensorEntityDescription(GreeEntityDescription, SensorEntityDescription value_fn=lambda device: device.room_humidity if device._has_room_humidity_sensor else None, available_fn=lambda device: device.available and device._has_room_humidity_sensor, ), + GreeSensorEntityDescription( + property_key="compressor_frequency", + device_class=SensorDeviceClass.FREQUENCY, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfFrequency.HERTZ, + entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:sine-wave", + value_fn=lambda device: device.compressor_frequency, + available_fn=lambda device: device.available and bool(device._has_compressor_freq), + ), + GreeSensorEntityDescription( + property_key="compressor_temperature", + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + suggested_display_precision=0, + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda device: device.compressor_temperature, + available_fn=lambda device: device.available and bool(device._has_compressor_temp), + ), + GreeSensorEntityDescription( + property_key="evaporator_temperature", + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + suggested_display_precision=0, + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda device: device.evaporator_temperature, + available_fn=lambda device: device.available and bool(device._has_evaporator_temp), + ), + GreeSensorEntityDescription( + property_key="env_temperature", + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + suggested_display_precision=0, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + value_fn=lambda device: device.env_temperature, + available_fn=lambda device: device.available and bool(device._has_env_temp), + ), + GreeSensorEntityDescription( + property_key="outside_temperature_alt", + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + suggested_display_precision=0, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + value_fn=lambda device: device.outside_temperature_alt, + available_fn=lambda device: device.available and bool(device._has_outside_temp_alt), + ), + GreeSensorEntityDescription( + property_key="pm25", + device_class=SensorDeviceClass.PM25, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + value_fn=lambda device: device.pm25, + available_fn=lambda device: device.available and bool(device._has_pm25), + ), + GreeSensorEntityDescription( + property_key="error_code", + entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:alert-circle-outline", + value_fn=lambda device: device.error_code, + available_fn=lambda device: device.available and bool(device._has_all_err), + ), + GreeSensorEntityDescription( + property_key="jf_error_code", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + icon="mdi:alert-circle-outline", + value_fn=lambda device: device.jf_error_code, + available_fn=lambda device: device.available and bool(device._has_jf_error), + ), ) @@ -81,7 +162,9 @@ def __init__(self, hass, entry, description: GreeSensorEntityDescription) -> Non super().__init__(hass, entry, description) # Set temperature unit for temperature sensors - if description.device_class == SensorDeviceClass.TEMPERATURE: + # Diagnostic temperatures declare Celsius explicitly (the protocol always reports + # them that way); only the user-facing ones follow the device's configured unit. + if description.device_class == SensorDeviceClass.TEMPERATURE and description.native_unit_of_measurement is None: self._attr_native_unit_of_measurement = self._device.temperature_unit @property diff --git a/custom_components/gree/switch.py b/custom_components/gree/switch.py index e9ebc3b9..36bc33dc 100644 --- a/custom_components/gree/switch.py +++ b/custom_components/gree/switch.py @@ -87,6 +87,26 @@ async def _set_beeper(device, value: bool) -> None: setattr(device, "_beeper_enabled", value) +async def _set_child_lock(device, value: bool) -> None: + await device.SyncState({"ChildLock": 1 if value else 0}) + + +async def _set_dazzling(device, value: bool) -> None: + await device.SyncState({"Dazzling": 1 if value else 0}) + + +async def _set_uvc(device, value: bool) -> None: + await device.SyncState({"UvcControl": 1 if value else 0}) + + +async def _set_auto_clean(device, value: bool) -> None: + await device.SyncState({"AutoClean": 1 if value else 0}) + + +async def _set_nobody_save(device, value: bool) -> None: + await device.SyncState({"NobodySave": 1 if value else 0}) + + SWITCHES: tuple[GreeSwitchEntityDescription, ...] = ( GreeSwitchEntityDescription( property_key="xfan", @@ -149,6 +169,41 @@ async def _set_beeper(device, value: bool) -> None: set_fn=_set_light_sensor, available_fn=lambda device: getattr(device, "_has_light_sensor", False), ), + GreeSwitchEntityDescription( + property_key="child_lock", + icon="mdi:lock", + value_fn=lambda device: device._acOptions.get("ChildLock") == 1, + set_fn=_set_child_lock, + available_fn=lambda device: getattr(device, "_has_child_lock", False), + ), + GreeSwitchEntityDescription( + property_key="dazzling", + icon="mdi:television-ambient-light", + value_fn=lambda device: device._acOptions.get("Dazzling") == 1, + set_fn=_set_dazzling, + available_fn=lambda device: getattr(device, "_has_dazzling", False), + ), + GreeSwitchEntityDescription( + property_key="uvc", + icon="mdi:bacteria", + value_fn=lambda device: device._acOptions.get("UvcControl") == 1, + set_fn=_set_uvc, + available_fn=lambda device: getattr(device, "_has_uvc", False), + ), + GreeSwitchEntityDescription( + property_key="auto_clean", + icon="mdi:broom", + value_fn=lambda device: device._acOptions.get("AutoClean") == 1, + set_fn=_set_auto_clean, + available_fn=lambda device: getattr(device, "_has_auto_clean", False), + ), + GreeSwitchEntityDescription( + property_key="nobody_save", + icon="mdi:account-off", + value_fn=lambda device: device._acOptions.get("NobodySave") == 1, + set_fn=_set_nobody_save, + available_fn=lambda device: getattr(device, "_has_nobody_save", False), + ), # These entities are not kept in the climate device GreeSwitchEntityDescription( property_key="auto_xfan", diff --git a/custom_components/gree/translations/en.json b/custom_components/gree/translations/en.json index 0d80a217..476b0994 100644 --- a/custom_components/gree/translations/en.json +++ b/custom_components/gree/translations/en.json @@ -60,10 +60,10 @@ "host": "IP Address", "port": "Port", "mac": "MAC Address", - "hvac_modes" : "HVAC Modes", - "fan_modes" : "Fan Modes", - "swing_modes" : "Vertical Swing Modes", - "swing_horizontal_modes" : "Horizontal Swing Modes", + "hvac_modes": "HVAC Modes", + "fan_modes": "Fan Modes", + "swing_modes": "Vertical Swing Modes", + "swing_horizontal_modes": "Horizontal Swing Modes", "encryption_key": "Encryption Key", "uid": "UID", "encryption_version": "Encryption Version", @@ -76,10 +76,10 @@ "init": { "title": "Gree Climate Options", "data": { - "hvac_modes" : "HVAC Modes", - "fan_modes" : "Fan Modes", - "swing_modes" : "Vertical Swing Modes", - "swing_horizontal_modes" : "Horizontal Swing Modes", + "hvac_modes": "HVAC Modes", + "fan_modes": "Fan Modes", + "swing_modes": "Vertical Swing Modes", + "swing_horizontal_modes": "Horizontal Swing Modes", "disable_available_check": "Disable Available Check", "temp_sensor_offset": "Temperature Sensor Offset" } @@ -210,6 +210,38 @@ "room_humidity": { "name": "Room Humidity", "description": "Shows the room humidity level measured by the air conditioner's internal sensor." + }, + "compressor_frequency": { + "name": "Compressor Frequency", + "description": "Operating frequency of the outdoor compressor. On multi-split systems this reflects the shared outdoor unit, not this indoor unit alone." + }, + "compressor_temperature": { + "name": "Compressor Temperature", + "description": "Temperature measured at the compressor." + }, + "evaporator_temperature": { + "name": "Evaporator Temperature", + "description": "Temperature of the indoor coil." + }, + "env_temperature": { + "name": "Ambient Temperature (secondary sensor)", + "description": "Indoor temperature from the device's secondary ambient sensor." + }, + "outside_temperature_alt": { + "name": "Outside Temperature (secondary sensor)", + "description": "Outdoor temperature from the device's secondary external sensor." + }, + "pm25": { + "name": "PM2.5", + "description": "Particulate matter measured by the air conditioner." + }, + "error_code": { + "name": "Error Code", + "description": "Aggregated fault code reported by the unit. 0 means no fault." + }, + "jf_error_code": { + "name": "Error Code (secondary register)", + "description": "Secondary fault register reported by the unit. 0 means no fault." } }, "switch": { @@ -260,6 +292,36 @@ "beeper": { "name": "Beeper", "description": "Controls the beeper sounds from the air conditioner unit. When enabled, the unit will make sounds for button presses and status changes." + }, + "child_lock": { + "name": "Child Lock", + "description": "Locks the unit's control panel and remote." + }, + "dazzling": { + "name": "Display Dimming", + "description": "Dims or blanks the unit's display." + }, + "uvc": { + "name": "UVC Sterilization", + "description": "Enables the UVC sterilization lamp." + }, + "auto_clean": { + "name": "Auto Clean", + "description": "Runs the self-cleaning cycle." + }, + "nobody_save": { + "name": "Absence Energy Saving", + "description": "Reduces output when no presence is detected." + } + }, + "binary_sensor": { + "filter_alarm": { + "name": "Filter Alarm", + "description": "On when the unit asks for filter cleaning." + }, + "hepa_alarm": { + "name": "HEPA Replacement", + "description": "On when the unit asks for HEPA filter replacement." } } } diff --git a/custom_components/gree/translations/pl.json b/custom_components/gree/translations/pl.json index 9da9ee11..bead0e45 100644 --- a/custom_components/gree/translations/pl.json +++ b/custom_components/gree/translations/pl.json @@ -223,6 +223,70 @@ "beeper": { "name": "Sygnał dźwiękowy", "description": "Włącza lub wyłącza sygnał dźwiękowy przy obsłudze klimatyzatora. Kiedy jest włączony jednostka wydaje dzwięki przy każdej operacji." + }, + "child_lock": { + "name": "Blokada rodzicielska", + "description": "Blokuje panel sterowania i pilota." + }, + "dazzling": { + "name": "Wygaszanie wyświetlacza", + "description": "Przygasza lub wygasza wyświetlacz urządzenia." + }, + "uvc": { + "name": "Sterylizacja UVC", + "description": "Włącza lampę sterylizacyjną UVC." + }, + "auto_clean": { + "name": "Samoczyszczenie", + "description": "Uruchamia cykl samoczyszczenia." + }, + "nobody_save": { + "name": "Oszczędzanie pod nieobecność", + "description": "Ogranicza pracę, gdy nie wykryto obecności." + } + }, + "sensor": { + "compressor_frequency": { + "name": "Częstotliwość sprężarki", + "description": "Częstotliwość pracy sprężarki agregatu. W układach multi-split dotyczy wspólnego agregatu, a nie tej jednostki wewnętrznej." + }, + "compressor_temperature": { + "name": "Temperatura sprężarki", + "description": "Temperatura zmierzona na sprężarce." + }, + "evaporator_temperature": { + "name": "Temperatura parownika", + "description": "Temperatura wymiennika jednostki wewnętrznej." + }, + "env_temperature": { + "name": "Temperatura otoczenia (czujnik dodatkowy)", + "description": "Temperatura wewnętrzna z dodatkowego czujnika urządzenia." + }, + "outside_temperature_alt": { + "name": "Temperatura zewnętrzna (czujnik dodatkowy)", + "description": "Temperatura zewnętrzna z dodatkowego czujnika urządzenia." + }, + "pm25": { + "name": "PM2.5", + "description": "Stężenie pyłu zawieszonego mierzone przez klimatyzator." + }, + "error_code": { + "name": "Kod błędu", + "description": "Zbiorczy kod usterki zgłaszany przez urządzenie. 0 oznacza brak usterki." + }, + "jf_error_code": { + "name": "Kod błędu (rejestr dodatkowy)", + "description": "Dodatkowy rejestr usterek zgłaszany przez urządzenie. 0 oznacza brak usterki." + } + }, + "binary_sensor": { + "filter_alarm": { + "name": "Alarm filtra", + "description": "Włączony, gdy urządzenie sygnalizuje potrzebę czyszczenia filtra." + }, + "hepa_alarm": { + "name": "Wymiana filtra HEPA", + "description": "Włączony, gdy urządzenie sygnalizuje potrzebę wymiany filtra HEPA." } } } From 9d508f9dd8e2e18c7963f46d8fbf971ba0f6dc04 Mon Sep 17 00:00:00 2001 From: Radek Piekarz Date: Sun, 13 Sep 2026 14:56:53 +0200 Subject: [PATCH 2/2] Use UnitOfDensity for the PM2.5 unit CONCENTRATION_MICROGRAMS_PER_CUBIC_METER is deprecated and Home Assistant logs a warning naming this integration; it goes away in core 2027.8. Import UnitOfDensity where available and fall back to the old constant so older cores keep working. --- custom_components/gree/sensor.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/custom_components/gree/sensor.py b/custom_components/gree/sensor.py index 7176c132..10234072 100644 --- a/custom_components/gree/sensor.py +++ b/custom_components/gree/sensor.py @@ -14,13 +14,19 @@ SensorStateClass, ) from homeassistant.const import ( - CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, PERCENTAGE, EntityCategory, UnitOfFrequency, UnitOfTemperature, ) +try: # UnitOfDensity replaced the standalone constant; keep older cores working. + from homeassistant.const import UnitOfDensity + + PM25_UNIT = UnitOfDensity.MICROGRAMS_PER_CUBIC_METER +except ImportError: + from homeassistant.const import CONCENTRATION_MICROGRAMS_PER_CUBIC_METER as PM25_UNIT + # Local imports from .const import DOMAIN @@ -110,7 +116,7 @@ class GreeSensorEntityDescription(GreeEntityDescription, SensorEntityDescription property_key="pm25", device_class=SensorDeviceClass.PM25, state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + native_unit_of_measurement=PM25_UNIT, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, value_fn=lambda device: device.pm25,