v0.5.4
This commit is contained in:
@@ -118,3 +118,9 @@ Bedroom -> GREE Bedroom + sensor.bedroom_temperature
|
||||
```
|
||||
|
||||
The recommended `combined` strategy keeps the GREE sensor as the primary input and uses the room sensor as a configurable supporting measurement (40% weight by default). A zone may also select the room sensor as its preferred source. If HA or that entity becomes unavailable, the controller falls back to the corresponding GREE unit, so local control and schedules continue to run.
|
||||
## Zone climate and optional unit features (0.5.4)
|
||||
|
||||
Each controller zone is exposed as a full Home Assistant `climate` entity with current/target temperature and HVAC modes: Off, Auto (follow the controller house mode), Cool and Heat. The existing zone target `number` and enabled `switch` remain available for compatibility.
|
||||
|
||||
When the controller detects optional GREE properties, the integration also creates switches for supported features such as panel light, Quiet, Turbo, X-FAN, Air, Health and native Sleep. Reload the integration (or restart Home Assistant) after upgrading so newly added entity types are created.
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ async def async_setup_entry(
|
||||
"""Create climate entities for all devices exposed by the controller."""
|
||||
runtime: GreeControllerRuntimeData = entry.runtime_data
|
||||
registry = er.async_get(hass)
|
||||
entities: list[GreeControllerClimate] = []
|
||||
entities: list[ClimateEntity] = []
|
||||
|
||||
for device_id, device in runtime.coordinator.data.items():
|
||||
desired_entity_id = runtime.entity_map.get(device_id)
|
||||
@@ -70,6 +70,11 @@ async def async_setup_entry(
|
||||
GreeControllerClimate(runtime.coordinator, device_id, desired_entity_id)
|
||||
)
|
||||
|
||||
for zone in runtime.coordinator.plan.get("zones", []):
|
||||
zone_id = str(zone.get("zone_id") or "").strip()
|
||||
if zone_id:
|
||||
entities.append(GreeControllerZoneClimate(runtime.coordinator, zone_id))
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
@@ -166,6 +171,10 @@ class GreeControllerClimate(CoordinatorEntity[GreeControllerCoordinator], Climat
|
||||
"quiet": bool(device.get("quiet", False)),
|
||||
"turbo": bool(device.get("turbo", False)),
|
||||
"light": bool(device.get("light", False)),
|
||||
"xfan": bool(device.get("xfan", False)),
|
||||
"air": bool(device.get("air", False)),
|
||||
"health": bool(device.get("health", False)),
|
||||
"sleep": bool(device.get("sleep", False)),
|
||||
"last_seen": device.get("last_seen"),
|
||||
}
|
||||
|
||||
@@ -202,3 +211,109 @@ class GreeControllerClimate(CoordinatorEntity[GreeControllerCoordinator], Climat
|
||||
|
||||
async def async_set_swing_horizontal_mode(self, swing_horizontal_mode: str) -> None:
|
||||
await self._command({"swing_horizontal": swing_horizontal_mode == SWING_ON})
|
||||
|
||||
class GreeControllerZoneClimate(CoordinatorEntity[GreeControllerCoordinator], ClimateEntity):
|
||||
"""Full climate entity for a controller thermostat zone."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_name = None
|
||||
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||
_attr_min_temp = 8.0
|
||||
_attr_max_temp = 30.0
|
||||
_attr_target_temperature_step = 0.5
|
||||
_attr_hvac_modes = [HVACMode.OFF, HVACMode.AUTO, HVACMode.COOL, HVACMode.HEAT]
|
||||
_attr_supported_features = (
|
||||
ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
| ClimateEntityFeature.TURN_ON
|
||||
| ClimateEntityFeature.TURN_OFF
|
||||
)
|
||||
|
||||
def __init__(self, coordinator: GreeControllerCoordinator, zone_id: str) -> None:
|
||||
super().__init__(coordinator)
|
||||
self._zone_id = zone_id
|
||||
self._attr_unique_id = f"{zone_id}-zone-climate"
|
||||
|
||||
@property
|
||||
def _zone(self) -> dict[str, Any]:
|
||||
for zone in self.coordinator.plan.get("zones", []):
|
||||
if str(zone.get("zone_id")) == self._zone_id:
|
||||
return zone
|
||||
return {}
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
return super().available and bool(self._zone)
|
||||
|
||||
@property
|
||||
def device_info(self) -> DeviceInfo:
|
||||
zone = self._zone
|
||||
return DeviceInfo(
|
||||
identifiers={(DOMAIN, f"zone:{self._zone_id}")},
|
||||
name=str(zone.get("zone_name") or self._zone_id),
|
||||
manufacturer="GREE Controller",
|
||||
model="Zone thermostat",
|
||||
via_device=(DOMAIN, "controller"),
|
||||
)
|
||||
|
||||
@property
|
||||
def current_temperature(self) -> float | None:
|
||||
value = self._zone.get("current_temperature")
|
||||
return float(value) if value is not None else None
|
||||
|
||||
@property
|
||||
def target_temperature(self) -> float | None:
|
||||
value = self._zone.get("target_temperature")
|
||||
return float(value) if value is not None else None
|
||||
|
||||
@property
|
||||
def hvac_mode(self) -> HVACMode:
|
||||
zone = self._zone
|
||||
if not zone.get("enabled", False):
|
||||
return HVACMode.OFF
|
||||
if zone.get("inherit_house_mode", True):
|
||||
return HVACMode.AUTO
|
||||
return HVACMode.HEAT if str(zone.get("configured_mode", zone.get("mode", "cool"))) == "heat" else HVACMode.COOL
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict[str, Any]:
|
||||
zone = self._zone
|
||||
return {
|
||||
"controller_zone_id": self._zone_id,
|
||||
"effective_mode": zone.get("mode"),
|
||||
"follows_house_mode": bool(zone.get("inherit_house_mode", True)),
|
||||
"preset": zone.get("preset"),
|
||||
"demand": bool(zone.get("demand", False)),
|
||||
"device_setpoint": zone.get("device_setpoint"),
|
||||
"control_source": zone.get("control_source"),
|
||||
"current_schedule": zone.get("current_schedule_name"),
|
||||
}
|
||||
|
||||
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
|
||||
if hvac_mode == HVACMode.OFF:
|
||||
payload = {"enabled": False}
|
||||
elif hvac_mode == HVACMode.AUTO:
|
||||
payload = {"enabled": True, "mode": "house"}
|
||||
elif hvac_mode == HVACMode.COOL:
|
||||
payload = {"enabled": True, "mode": "cool"}
|
||||
elif hvac_mode == HVACMode.HEAT:
|
||||
payload = {"enabled": True, "mode": "heat"}
|
||||
else:
|
||||
return
|
||||
await self.coordinator.client.zone_control(self._zone_id, payload)
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||
temperature = kwargs.get(ATTR_TEMPERATURE)
|
||||
if temperature is None:
|
||||
return
|
||||
await self.coordinator.client.zone_control(self._zone_id, {"setpoint": float(temperature)})
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
async def async_turn_on(self) -> None:
|
||||
await self.coordinator.client.zone_control(self._zone_id, {"enabled": True})
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
async def async_turn_off(self) -> None:
|
||||
await self.coordinator.client.zone_control(self._zone_id, {"enabled": False})
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"domain": "gree_controller",
|
||||
"name": "GREE Controller",
|
||||
"version": "0.5.3",
|
||||
"version": "0.5.4",
|
||||
"config_flow": true,
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
|
||||
@@ -16,6 +16,17 @@ from .const import DOMAIN
|
||||
from .coordinator import GreeControllerCoordinator
|
||||
|
||||
|
||||
DEVICE_FEATURE_SWITCHES = [
|
||||
("light", "supports_light", "Panel light"),
|
||||
("quiet", "supports_quiet", "Quiet"),
|
||||
("turbo", "supports_turbo", "Turbo"),
|
||||
("xfan", "supports_xfan", "X-FAN"),
|
||||
("air", "supports_air", "Air"),
|
||||
("health", "supports_health", "Health"),
|
||||
("sleep", "supports_sleep", "Sleep"),
|
||||
]
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
@@ -23,11 +34,15 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Create one enabled switch per controller zone."""
|
||||
runtime: GreeControllerRuntimeData = entry.runtime_data
|
||||
entities = [
|
||||
entities: list[SwitchEntity] = [
|
||||
GreeControllerZoneEnabledSwitch(runtime.coordinator, str(zone["zone_id"]))
|
||||
for zone in runtime.coordinator.plan.get("zones", [])
|
||||
if zone.get("zone_id")
|
||||
]
|
||||
for device_id, device in runtime.coordinator.data.items():
|
||||
for field, support_field, name in DEVICE_FEATURE_SWITCHES:
|
||||
if device.get(support_field) is True:
|
||||
entities.append(GreeControllerDeviceFeatureSwitch(runtime.coordinator, device_id, field, support_field, name))
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
@@ -75,3 +90,61 @@ class GreeControllerZoneEnabledSwitch(CoordinatorEntity[GreeControllerCoordinato
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
await self.coordinator.client.zone_control(self._zone_id, {"enabled": False})
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
class GreeControllerDeviceFeatureSwitch(CoordinatorEntity[GreeControllerCoordinator], SwitchEntity):
|
||||
"""Optional GREE unit feature exposed only when the controller detected it."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: GreeControllerCoordinator,
|
||||
device_id: str,
|
||||
field: str,
|
||||
support_field: str,
|
||||
name: str,
|
||||
) -> None:
|
||||
super().__init__(coordinator)
|
||||
self._device_id = device_id
|
||||
self._field = field
|
||||
self._support_field = support_field
|
||||
self._attr_name = name
|
||||
self._attr_unique_id = f"{device_id}-{field}"
|
||||
|
||||
@property
|
||||
def _device(self) -> dict[str, Any]:
|
||||
return self.coordinator.data.get(self._device_id, {})
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
return (
|
||||
super().available
|
||||
and bool(self._device.get("online", False))
|
||||
and self._device.get(self._support_field) is True
|
||||
)
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
return bool(self._device.get(self._field, False))
|
||||
|
||||
@property
|
||||
def device_info(self) -> DeviceInfo:
|
||||
device = self._device
|
||||
return DeviceInfo(
|
||||
identifiers={(DOMAIN, self._device_id)},
|
||||
name=str(device.get("name") or self._device_id),
|
||||
manufacturer="GREE",
|
||||
model=str(device.get("model") or "GREE HVAC"),
|
||||
sw_version=str(device.get("firmware") or "") or None,
|
||||
)
|
||||
|
||||
async def _set(self, value: bool) -> None:
|
||||
await self.coordinator.client.command(self._device_id, {self._field: value})
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
await self._set(True)
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
await self._set(False)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user