v0.5.0
This commit is contained in:
@@ -74,3 +74,21 @@ class GreeControllerClient:
|
||||
f"/api/integrations/home-assistant/devices/{device_id}/command",
|
||||
json=payload,
|
||||
)
|
||||
|
||||
async def control_plan(self) -> dict[str, Any]:
|
||||
"""Return the current whole-house and zone automation plan."""
|
||||
data = await self._request("GET", "/api/integrations/home-assistant/control-plan")
|
||||
if not isinstance(data, dict):
|
||||
raise GreeControllerApiError("Controller returned an invalid control plan 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(
|
||||
"POST",
|
||||
f"/api/integrations/home-assistant/zones/{zone_id}/control",
|
||||
json=payload,
|
||||
)
|
||||
if not isinstance(data, dict):
|
||||
raise GreeControllerApiError("Controller returned an invalid zone payload")
|
||||
return data
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from homeassistant.const import Platform
|
||||
|
||||
DOMAIN = "gree_controller"
|
||||
PLATFORMS = [Platform.CLIMATE]
|
||||
PLATFORMS = [Platform.CLIMATE, Platform.SENSOR, Platform.NUMBER, Platform.SWITCH]
|
||||
|
||||
CONF_URL = "url"
|
||||
CONF_TOKEN = "token"
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -27,10 +29,15 @@ class GreeControllerCoordinator(DataUpdateCoordinator[dict[str, dict]]):
|
||||
update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL_SECONDS),
|
||||
)
|
||||
self.client = client
|
||||
self.plan: dict[str, Any] = {}
|
||||
|
||||
async def _async_update_data(self) -> dict[str, dict]:
|
||||
try:
|
||||
devices = await self.client.devices()
|
||||
devices, plan = await asyncio.gather(
|
||||
self.client.devices(),
|
||||
self.client.control_plan(),
|
||||
)
|
||||
except GreeControllerApiError as err:
|
||||
raise UpdateFailed(str(err)) from err
|
||||
self.plan = plan
|
||||
return {str(device["id"]): device for device in devices if device.get("id")}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"domain": "gree_controller",
|
||||
"name": "GREE Controller",
|
||||
"version": "0.4.5",
|
||||
"version": "0.5.0",
|
||||
"config_flow": true,
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
"""Writable zone setpoint numbers for GREE Controller."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.number import NumberDeviceClass, NumberEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import UnitOfTemperature
|
||||
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
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Create one writable target-temperature number per zone."""
|
||||
runtime: GreeControllerRuntimeData = entry.runtime_data
|
||||
entities = [
|
||||
GreeControllerZoneTargetNumber(runtime.coordinator, str(zone["zone_id"]))
|
||||
for zone in runtime.coordinator.plan.get("zones", [])
|
||||
if zone.get("zone_id")
|
||||
]
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class GreeControllerZoneTargetNumber(CoordinatorEntity[GreeControllerCoordinator], NumberEntity):
|
||||
"""Zone target override controlled through the standalone service."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_name = "Target temperature"
|
||||
_attr_device_class = NumberDeviceClass.TEMPERATURE
|
||||
_attr_native_unit_of_measurement = UnitOfTemperature.CELSIUS
|
||||
_attr_native_min_value = 8.0
|
||||
_attr_native_max_value = 30.0
|
||||
_attr_native_step = 0.5
|
||||
|
||||
def __init__(self, coordinator: GreeControllerCoordinator, zone_id: str) -> None:
|
||||
super().__init__(coordinator)
|
||||
self._zone_id = zone_id
|
||||
self._attr_unique_id = f"{zone_id}-target-temperature"
|
||||
|
||||
@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 native_value(self) -> float | None:
|
||||
value = self._zone.get("target_temperature")
|
||||
return float(value) if value is not None else None
|
||||
|
||||
@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"),
|
||||
)
|
||||
|
||||
async def async_set_native_value(self, value: float) -> None:
|
||||
await self.coordinator.client.zone_control(self._zone_id, {"setpoint": float(value)})
|
||||
await self.coordinator.async_request_refresh()
|
||||
@@ -0,0 +1,126 @@
|
||||
"""Automation-plan sensors exposed by GREE Controller."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.sensor import SensorEntity
|
||||
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
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Create whole-house and per-zone automation-plan sensors."""
|
||||
runtime: GreeControllerRuntimeData = entry.runtime_data
|
||||
entities: list[SensorEntity] = [GreeControllerHousePlanSensor(runtime.coordinator)]
|
||||
for zone in runtime.coordinator.plan.get("zones", []):
|
||||
if zone.get("zone_id"):
|
||||
entities.append(GreeControllerZonePlanSensor(runtime.coordinator, str(zone["zone_id"])))
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class GreeControllerHousePlanSensor(CoordinatorEntity[GreeControllerCoordinator], SensorEntity):
|
||||
"""Summary of the current whole-house control plan."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_name = "Automation plan"
|
||||
_attr_unique_id = "house-control-plan"
|
||||
_unrecorded_attributes = frozenset({"next_events", "rules"})
|
||||
|
||||
@property
|
||||
def native_value(self) -> str:
|
||||
return str(self.coordinator.plan.get("house_mode") or "unknown")
|
||||
|
||||
@property
|
||||
def device_info(self) -> DeviceInfo:
|
||||
return DeviceInfo(
|
||||
identifiers={(DOMAIN, "controller")},
|
||||
name="GREE Controller",
|
||||
manufacturer="GREE Controller",
|
||||
model="Local controller",
|
||||
)
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict[str, Any]:
|
||||
plan = self.coordinator.plan
|
||||
zones = plan.get("zones", [])
|
||||
return {
|
||||
"generated_at": plan.get("generated_at"),
|
||||
"outdoor_temperature": plan.get("outdoor_temperature"),
|
||||
"control_strategy": plan.get("control_strategy"),
|
||||
"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", []),
|
||||
"rules": plan.get("rules", []),
|
||||
}
|
||||
|
||||
|
||||
class GreeControllerZonePlanSensor(CoordinatorEntity[GreeControllerCoordinator], SensorEntity):
|
||||
"""Readable automation plan for one controller zone."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_name = "Control plan"
|
||||
_unrecorded_attributes = frozenset({"next_events"})
|
||||
|
||||
def __init__(self, coordinator: GreeControllerCoordinator, zone_id: str) -> None:
|
||||
super().__init__(coordinator)
|
||||
self._zone_id = zone_id
|
||||
self._attr_unique_id = f"{zone_id}-control-plan"
|
||||
|
||||
@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 native_value(self) -> str:
|
||||
zone = self._zone
|
||||
if not zone.get("enabled", False):
|
||||
return "disabled"
|
||||
return "requesting" if zone.get("demand", False) else "satisfied"
|
||||
|
||||
@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 extra_state_attributes(self) -> dict[str, Any]:
|
||||
zone = self._zone
|
||||
return {
|
||||
"zone_id": self._zone_id,
|
||||
"device_id": zone.get("device_id"),
|
||||
"device_name": zone.get("device_name"),
|
||||
"mode": zone.get("mode"),
|
||||
"preset": zone.get("preset"),
|
||||
"current_temperature": zone.get("current_temperature"),
|
||||
"target_temperature": zone.get("target_temperature"),
|
||||
"device_setpoint": zone.get("device_setpoint"),
|
||||
"control_source": zone.get("control_source"),
|
||||
"manual_override_until": zone.get("manual_override_until"),
|
||||
"current_schedule": zone.get("current_schedule_name"),
|
||||
"next_events": zone.get("next_events", []),
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
"""Writable zone enabled switches for GREE Controller."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.switch import SwitchEntity
|
||||
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
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Create one enabled switch per controller zone."""
|
||||
runtime: GreeControllerRuntimeData = entry.runtime_data
|
||||
entities = [
|
||||
GreeControllerZoneEnabledSwitch(runtime.coordinator, str(zone["zone_id"]))
|
||||
for zone in runtime.coordinator.plan.get("zones", [])
|
||||
if zone.get("zone_id")
|
||||
]
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class GreeControllerZoneEnabledSwitch(CoordinatorEntity[GreeControllerCoordinator], SwitchEntity):
|
||||
"""Enable or disable a controller thermostat zone."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_name = "Enabled"
|
||||
|
||||
def __init__(self, coordinator: GreeControllerCoordinator, zone_id: str) -> None:
|
||||
super().__init__(coordinator)
|
||||
self._zone_id = zone_id
|
||||
self._attr_unique_id = f"{zone_id}-enabled"
|
||||
|
||||
@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 is_on(self) -> bool:
|
||||
return bool(self._zone.get("enabled", False))
|
||||
|
||||
@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"),
|
||||
)
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
await self.coordinator.client.zone_control(self._zone_id, {"enabled": True})
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
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()
|
||||
Reference in New Issue
Block a user