This commit is contained in:
Mateusz Gruszczyński
2026-08-25 09:31:15 +02:00
parent 25e1a854e3
commit fd26d6877f
25 changed files with 460 additions and 99 deletions
+9
View File
@@ -126,3 +126,12 @@ From version 0.6.4, zone climate entities use `climate.<zone_name>_thermostat`,
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.
## Whole-house controls
The integration also exposes three controller-level entities on the **GREE Controller** device:
- **Thermostat mode** (`select`) — Cooling, Heating or **Do not control**. Do not control pauses house-level thermostat commands but leaves direct device control and explicit per-zone Heat/Cool overrides untouched.
- **Work profile** (`select`) — Auto schedule, Comfort, Sleep or Away. If zones have mixed manual profiles, the select has no single current option until a whole-house profile is chosen again.
- **All air conditioners** (`switch`) — separate master power. Turning it off powers every enabled unit down and prevents zones/controller automations from restarting them. Turning it on powers all enabled units on again without changing the selected thermostat mode or profile.
These controls use the Home Assistant integration token and the dedicated `/api/integrations/home-assistant/house/*` endpoints.
@@ -82,6 +82,39 @@ class GreeControllerClient:
raise GreeControllerApiError("Controller returned an invalid control plan payload")
return data
async def house_control(self, mode: str) -> dict[str, Any]:
"""Change the whole-house thermostat mode."""
data = await self._request(
"POST",
"/api/integrations/home-assistant/house/control",
json={"mode": mode},
)
if not isinstance(data, dict):
raise GreeControllerApiError("Controller returned an invalid house control payload")
return data
async def house_preset(self, preset: str) -> dict[str, Any]:
"""Change the whole-house work profile."""
data = await self._request(
"POST",
"/api/integrations/home-assistant/house/preset",
json={"preset": preset},
)
if not isinstance(data, dict):
raise GreeControllerApiError("Controller returned an invalid house preset payload")
return data
async def house_power(self, power: bool) -> dict[str, Any]:
"""Turn all enabled air conditioners on or off."""
data = await self._request(
"POST",
"/api/integrations/home-assistant/house/power",
json={"power": power},
)
if not isinstance(data, dict):
raise GreeControllerApiError("Controller returned an invalid house power payload")
return data
async def zone_control(self, zone_id: str, payload: dict[str, Any]) -> dict[str, Any]:
"""Change a zone thermostat override through the controller."""
data = await self._request(
@@ -3,7 +3,7 @@
from homeassistant.const import Platform
DOMAIN = "gree_controller"
PLATFORMS = [Platform.CLIMATE, Platform.SENSOR, Platform.NUMBER, Platform.SWITCH]
PLATFORMS = [Platform.CLIMATE, Platform.SENSOR, Platform.NUMBER, Platform.SELECT, Platform.SWITCH]
CONF_URL = "url"
CONF_TOKEN = "token"
@@ -1,7 +1,7 @@
{
"domain": "gree_controller",
"name": "GREE Controller",
"version": "0.6.4",
"version": "0.6.5",
"config_flow": true,
"integration_type": "hub",
"iot_class": "local_polling",
@@ -0,0 +1,105 @@
"""Whole-house thermostat controls for GREE Controller."""
from __future__ import annotations
from typing import Any
from homeassistant.components.select import SelectEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import GreeControllerRuntimeData
from .const import DOMAIN
from .coordinator import GreeControllerCoordinator
HOUSE_MODE_LABELS = {
"off": "Do not control",
"cool": "Cooling",
"heat": "Heating",
}
HOUSE_MODE_VALUES = {label: value for value, label in HOUSE_MODE_LABELS.items()}
HOUSE_PRESET_LABELS = {
"auto": "Auto schedule",
"comfort": "Comfort",
"sleep": "Sleep",
"away": "Away",
}
HOUSE_PRESET_VALUES = {label: value for value, label in HOUSE_PRESET_LABELS.items()}
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Create whole-house thermostat mode and work-profile selectors."""
runtime: GreeControllerRuntimeData = entry.runtime_data
async_add_entities(
[
GreeControllerHouseModeSelect(runtime.coordinator),
GreeControllerHousePresetSelect(runtime.coordinator),
]
)
class _GreeControllerHouseSelect(CoordinatorEntity[GreeControllerCoordinator], SelectEntity):
"""Base for controller-level selectors."""
_attr_has_entity_name = True
@property
def device_info(self) -> DeviceInfo:
return DeviceInfo(
identifiers={(DOMAIN, "controller")},
name="GREE Controller",
manufacturer="GREE Controller",
model="Local controller",
)
class GreeControllerHouseModeSelect(_GreeControllerHouseSelect):
"""Select the smart thermostat house mode."""
_attr_name = "Thermostat mode"
_attr_unique_id = "house-thermostat-mode"
_attr_options = list(HOUSE_MODE_VALUES)
@property
def current_option(self) -> str | None:
value = str(self.coordinator.plan.get("house_mode") or "")
return HOUSE_MODE_LABELS.get(value)
async def async_select_option(self, option: str) -> None:
value = HOUSE_MODE_VALUES.get(option)
if value is None:
return
await self.coordinator.client.house_control(value)
await self.coordinator.async_request_refresh()
class GreeControllerHousePresetSelect(_GreeControllerHouseSelect):
"""Select the whole-house thermostat work profile."""
_attr_name = "Work profile"
_attr_unique_id = "house-work-profile"
_attr_options = list(HOUSE_PRESET_VALUES)
@property
def current_option(self) -> str | None:
value = self.coordinator.plan.get("house_preset")
return HOUSE_PRESET_LABELS.get(str(value)) if value is not None else None
@property
def extra_state_attributes(self) -> dict[str, Any]:
return {"mixed_zone_profiles": self.coordinator.plan.get("house_preset") is None}
async def async_select_option(self, option: str) -> None:
value = HOUSE_PRESET_VALUES.get(option)
if value is None:
return
await self.coordinator.client.house_preset(value)
await self.coordinator.async_request_refresh()
@@ -59,6 +59,8 @@ class GreeControllerHousePlanSensor(CoordinatorEntity[GreeControllerCoordinator]
"generated_at": plan.get("generated_at"),
"outdoor_temperature": plan.get("outdoor_temperature"),
"control_strategy": plan.get("control_strategy"),
"work_profile": plan.get("house_preset"),
"master_power": bool(plan.get("house_power", False)),
"enabled_zones": sum(bool(zone.get("enabled")) for zone in zones),
"demanding_zones": sum(bool(zone.get("enabled")) and bool(zone.get("demand")) for zone in zones),
"next_events": plan.get("next_events", []),
@@ -35,9 +35,12 @@ async def async_setup_entry(
"""Create one enabled switch per controller zone."""
runtime: GreeControllerRuntimeData = entry.runtime_data
entities: list[SwitchEntity] = [
GreeControllerHousePowerSwitch(runtime.coordinator),
*[
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:
@@ -46,6 +49,35 @@ async def async_setup_entry(
async_add_entities(entities)
class GreeControllerHousePowerSwitch(CoordinatorEntity[GreeControllerCoordinator], SwitchEntity):
"""Master power action for all enabled indoor units."""
_attr_has_entity_name = True
_attr_name = "All air conditioners"
_attr_unique_id = "house-power-all"
@property
def is_on(self) -> bool:
return bool(self.coordinator.plan.get("house_power", False))
@property
def device_info(self) -> DeviceInfo:
return DeviceInfo(
identifiers={(DOMAIN, "controller")},
name="GREE Controller",
manufacturer="GREE Controller",
model="Local controller",
)
async def async_turn_on(self, **kwargs: Any) -> None:
await self.coordinator.client.house_power(True)
await self.coordinator.async_request_refresh()
async def async_turn_off(self, **kwargs: Any) -> None:
await self.coordinator.client.house_power(False)
await self.coordinator.async_request_refresh()
class GreeControllerZoneEnabledSwitch(CoordinatorEntity[GreeControllerCoordinator], SwitchEntity):
"""Enable or disable a controller thermostat zone."""