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..10234072 100644 --- a/custom_components/gree/sensor.py +++ b/custom_components/gree/sensor.py @@ -15,8 +15,18 @@ ) from homeassistant.const import ( 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 @@ -50,6 +60,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=PM25_UNIT, + 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 +168,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." } } }