diff --git a/custom_components/gree_controller/climate.py b/custom_components/gree_controller/climate.py index 6c06b2a..650eb1a 100644 --- a/custom_components/gree_controller/climate.py +++ b/custom_components/gree_controller/climate.py @@ -39,6 +39,40 @@ HA_TO_MODE = {value: key for key, value in MODE_TO_HA.items()} FAN_TO_NAME = {0: "auto", 1: "low", 2: "medium_low", 3: "medium", 4: "medium_high", 5: "high"} NAME_TO_FAN = {value: key for key, value in FAN_TO_NAME.items()} +VERTICAL_SWING_TO_VALUE = { + SWING_OFF: 0, + SWING_ON: 1, + "fixed_upper": 2, + "fixed_upper_middle": 3, + "fixed_middle": 4, + "fixed_lower_middle": 5, + "fixed_lower": 6, + "swing_upper": 7, + "swing_upper_middle": 8, + "swing_middle": 9, + "swing_lower_middle": 10, + "swing_lower": 11, +} +HORIZONTAL_SWING_TO_VALUE = { + SWING_OFF: 0, + SWING_ON: 1, + "fixed_left": 2, + "fixed_left_middle": 3, + "fixed_middle": 4, + "fixed_right_middle": 5, + "fixed_right": 6, +} +VALUE_TO_VERTICAL_SWING = {value: mode for mode, value in VERTICAL_SWING_TO_VALUE.items()} +VALUE_TO_HORIZONTAL_SWING = {value: mode for mode, value in HORIZONTAL_SWING_TO_VALUE.items()} + + +def _louver_mode(raw: Any, modes: dict[int, str]) -> str: + try: + value = int(raw) + except (TypeError, ValueError): + value = 0 + return modes.get(value, SWING_OFF) + async def async_setup_entry( hass: HomeAssistant, @@ -126,14 +160,15 @@ class GreeControllerClimate(CoordinatorEntity[GreeControllerCoordinator], Climat """Home Assistant climate entity controlled through the Rust service.""" _attr_has_entity_name = True + _attr_translation_key = "direct_control" _attr_temperature_unit = UnitOfTemperature.CELSIUS _attr_min_temp = 8.0 _attr_max_temp = 30.0 _attr_target_temperature_step = 1.0 _attr_hvac_modes = [HVACMode.OFF, HVACMode.AUTO, HVACMode.COOL, HVACMode.HEAT, HVACMode.DRY, HVACMode.FAN_ONLY] _attr_fan_modes = list(NAME_TO_FAN) - _attr_swing_modes = [SWING_OFF, SWING_ON] - _attr_swing_horizontal_modes = [SWING_OFF, SWING_ON] + _attr_swing_modes = list(VERTICAL_SWING_TO_VALUE) + _attr_swing_horizontal_modes = list(HORIZONTAL_SWING_TO_VALUE) _attr_supported_features = ( ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE @@ -200,11 +235,11 @@ class GreeControllerClimate(CoordinatorEntity[GreeControllerCoordinator], Climat @property def swing_mode(self) -> str: - return SWING_ON if self._device.get("swing_vertical", False) else SWING_OFF + return _louver_mode(self._device.get("swing_vertical", 0), VALUE_TO_VERTICAL_SWING) @property def swing_horizontal_mode(self) -> str: - return SWING_ON if self._device.get("swing_horizontal", False) else SWING_OFF + return _louver_mode(self._device.get("swing_horizontal", 0), VALUE_TO_HORIZONTAL_SWING) @property def extra_state_attributes(self) -> dict[str, Any]: @@ -219,6 +254,8 @@ class GreeControllerClimate(CoordinatorEntity[GreeControllerCoordinator], Climat "air": bool(device.get("air", False)), "health": bool(device.get("health", False)), "sleep": bool(device.get("sleep", False)), + "vertical_louver_position": int(device.get("swing_vertical", 0) or 0), + "horizontal_louver_position": int(device.get("swing_horizontal", 0) or 0), "last_seen": device.get("last_seen"), } @@ -250,15 +287,20 @@ class GreeControllerClimate(CoordinatorEntity[GreeControllerCoordinator], Climat await self._command({"fan_speed": NAME_TO_FAN[fan_mode]}) async def async_set_swing_mode(self, swing_mode: str) -> None: - await self._command({"swing_vertical": swing_mode == SWING_ON}) + value = VERTICAL_SWING_TO_VALUE.get(swing_mode) + if value is not None: + await self._command({"swing_vertical": value}) async def async_set_swing_horizontal_mode(self, swing_horizontal_mode: str) -> None: - await self._command({"swing_horizontal": swing_horizontal_mode == SWING_ON}) + value = HORIZONTAL_SWING_TO_VALUE.get(swing_horizontal_mode) + if value is not None: + await self._command({"swing_horizontal": value}) class GreeControllerZoneClimate(CoordinatorEntity[GreeControllerCoordinator], ClimateEntity): """Full climate entity for a controller thermostat zone.""" _attr_has_entity_name = True + _attr_translation_key = "zone_thermostat" _attr_name = "Thermostat" _attr_temperature_unit = UnitOfTemperature.CELSIUS _attr_min_temp = 8.0 @@ -266,8 +308,8 @@ class GreeControllerZoneClimate(CoordinatorEntity[GreeControllerCoordinator], Cl _attr_target_temperature_step = 0.5 _attr_hvac_modes = [HVACMode.OFF, HVACMode.AUTO, HVACMode.COOL, HVACMode.HEAT] _attr_preset_modes = ["auto", "comfort", "sleep", "away"] - _attr_swing_modes = [SWING_OFF, SWING_ON] - _attr_swing_horizontal_modes = [SWING_OFF, SWING_ON] + _attr_swing_modes = list(VERTICAL_SWING_TO_VALUE) + _attr_swing_horizontal_modes = list(HORIZONTAL_SWING_TO_VALUE) def __init__(self, coordinator: GreeControllerCoordinator, zone_id: str) -> None: super().__init__(coordinator) @@ -343,11 +385,11 @@ class GreeControllerZoneClimate(CoordinatorEntity[GreeControllerCoordinator], Cl @property def swing_mode(self) -> str: - return SWING_ON if self._device.get("swing_vertical", False) else SWING_OFF + return _louver_mode(self._device.get("swing_vertical", 0), VALUE_TO_VERTICAL_SWING) @property def swing_horizontal_mode(self) -> str: - return SWING_ON if self._device.get("swing_horizontal", False) else SWING_OFF + return _louver_mode(self._device.get("swing_horizontal", 0), VALUE_TO_HORIZONTAL_SWING) @property def extra_state_attributes(self) -> dict[str, Any]: @@ -378,6 +420,8 @@ class GreeControllerZoneClimate(CoordinatorEntity[GreeControllerCoordinator], Cl "actual_mode": zone.get("actual_mode"), "actual_setpoint": zone.get("actual_setpoint"), "current_schedule": zone.get("current_schedule_name"), + "vertical_louver_position": int(self._device.get("swing_vertical", 0) or 0), + "horizontal_louver_position": int(self._device.get("swing_horizontal", 0) or 0), } async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: @@ -406,17 +450,15 @@ class GreeControllerZoneClimate(CoordinatorEntity[GreeControllerCoordinator], Cl async def async_set_swing_mode(self, swing_mode: str) -> None: device_id = str(self._zone.get("device_id") or "").strip() - if device_id: - await self.coordinator.async_device_command( - device_id, {"swing_vertical": swing_mode == SWING_ON} - ) + value = VERTICAL_SWING_TO_VALUE.get(swing_mode) + if device_id and value is not None: + await self.coordinator.async_device_command(device_id, {"swing_vertical": value}) async def async_set_swing_horizontal_mode(self, swing_horizontal_mode: str) -> None: device_id = str(self._zone.get("device_id") or "").strip() - if device_id: - await self.coordinator.async_device_command( - device_id, {"swing_horizontal": swing_horizontal_mode == SWING_ON} - ) + value = HORIZONTAL_SWING_TO_VALUE.get(swing_horizontal_mode) + if device_id and value is not None: + await self.coordinator.async_device_command(device_id, {"swing_horizontal": value}) async def async_turn_on(self) -> None: await self.coordinator.async_zone_control(self._zone_id, {"power": True}) diff --git a/custom_components/gree_controller/coordinator.py b/custom_components/gree_controller/coordinator.py index fc3634d..b59a630 100644 --- a/custom_components/gree_controller/coordinator.py +++ b/custom_components/gree_controller/coordinator.py @@ -67,6 +67,10 @@ class GreeControllerCoordinator(DataUpdateCoordinator[dict[str, dict]]): expected["target_temperature"] = float(int(value + 0.5)) if "fan_speed" in expected: expected["fan_speed"] = min(5, max(0, int(expected["fan_speed"]))) + if "swing_vertical" in expected: + expected["swing_vertical"] = min(11, max(0, int(expected["swing_vertical"]))) + if "swing_horizontal" in expected: + expected["swing_horizontal"] = min(6, max(0, int(expected["swing_horizontal"]))) return expected def _overlay_pending_device_commands(self, devices: dict[str, dict[str, Any]]) -> None: diff --git a/custom_components/gree_controller/manifest.json b/custom_components/gree_controller/manifest.json index 90e7ec9..ee5b094 100644 --- a/custom_components/gree_controller/manifest.json +++ b/custom_components/gree_controller/manifest.json @@ -1,7 +1,7 @@ { "domain": "gree_controller", "name": "GREE Controller", - "version": "0.14.24", + "version": "0.15.0", "config_flow": true, "integration_type": "hub", "iot_class": "local_polling", diff --git a/custom_components/gree_controller/translations/en.json b/custom_components/gree_controller/translations/en.json index 898f7a1..26bb1dd 100644 --- a/custom_components/gree_controller/translations/en.json +++ b/custom_components/gree_controller/translations/en.json @@ -18,5 +18,91 @@ "abort": { "already_configured": "GREE Controller is already configured" } + }, + "entity": { + "climate": { + "direct_control": { + "name": "Direct control", + "state_attributes": { + "fan_mode": { + "state": { + "auto": "Auto", + "low": "Low", + "medium_low": "Medium-low", + "medium": "Medium", + "medium_high": "Medium-high", + "high": "High" + } + }, + "swing_mode": { + "state": { + "off": "Off", + "on": "Full range (Auto)", + "fixed_upper": "Fixed: Top", + "fixed_upper_middle": "Fixed: Upper-middle", + "fixed_middle": "Fixed: Middle", + "fixed_lower_middle": "Fixed: Lower-middle", + "fixed_lower": "Fixed: Bottom", + "swing_upper": "Swing: Top", + "swing_upper_middle": "Swing: Upper-middle", + "swing_middle": "Swing: Middle", + "swing_lower_middle": "Swing: Lower-middle", + "swing_lower": "Swing: Bottom" + } + }, + "swing_horizontal_mode": { + "state": { + "off": "Off", + "on": "Full range (Auto)", + "fixed_left": "Fixed: Left", + "fixed_left_middle": "Fixed: Left-middle", + "fixed_middle": "Fixed: Middle", + "fixed_right_middle": "Fixed: Middle-right", + "fixed_right": "Fixed: Right" + } + } + } + }, + "zone_thermostat": { + "name": "Thermostat", + "state_attributes": { + "preset_mode": { + "state": { + "auto": "Auto schedule", + "comfort": "Comfort", + "sleep": "Sleep", + "away": "Away" + } + }, + "swing_mode": { + "state": { + "off": "Off", + "on": "Full range (Auto)", + "fixed_upper": "Fixed: Top", + "fixed_upper_middle": "Fixed: Upper-middle", + "fixed_middle": "Fixed: Middle", + "fixed_lower_middle": "Fixed: Lower-middle", + "fixed_lower": "Fixed: Bottom", + "swing_upper": "Swing: Top", + "swing_upper_middle": "Swing: Upper-middle", + "swing_middle": "Swing: Middle", + "swing_lower_middle": "Swing: Lower-middle", + "swing_lower": "Swing: Bottom" + } + }, + "swing_horizontal_mode": { + "state": { + "off": "Off", + "on": "Full range (Auto)", + "fixed_left": "Fixed: Left", + "fixed_left_middle": "Fixed: Left-middle", + "fixed_middle": "Fixed: Middle", + "fixed_right_middle": "Fixed: Middle-right", + "fixed_right": "Fixed: Right" + } + } + } + } + } } } diff --git a/custom_components/gree_controller/translations/pl.json b/custom_components/gree_controller/translations/pl.json index bf877a9..d998502 100644 --- a/custom_components/gree_controller/translations/pl.json +++ b/custom_components/gree_controller/translations/pl.json @@ -18,5 +18,91 @@ "abort": { "already_configured": "GREE Controller jest już skonfigurowany" } + }, + "entity": { + "climate": { + "direct_control": { + "name": "Sterowanie bezpośrednie", + "state_attributes": { + "fan_mode": { + "state": { + "auto": "Automatyczny", + "low": "Niski", + "medium_low": "Średnio-niski", + "medium": "Średni", + "medium_high": "Średnio-wysoki", + "high": "Wysoki" + } + }, + "swing_mode": { + "state": { + "off": "Wyłączony", + "on": "Pełny zakres (Auto)", + "fixed_upper": "Stała: Góra", + "fixed_upper_middle": "Stała: Środek-góra", + "fixed_middle": "Stała: Środek", + "fixed_lower_middle": "Stała: Środek-dół", + "fixed_lower": "Stała: Dół", + "swing_upper": "Ruch: Góra", + "swing_upper_middle": "Ruch: Środek-góra", + "swing_middle": "Ruch: Środek", + "swing_lower_middle": "Ruch: Środek-dół", + "swing_lower": "Ruch: Dół" + } + }, + "swing_horizontal_mode": { + "state": { + "off": "Wyłączony", + "on": "Pełny zakres (Auto)", + "fixed_left": "Stała: Lewo", + "fixed_left_middle": "Stała: Lewo-środek", + "fixed_middle": "Stała: Środek", + "fixed_right_middle": "Stała: Środek-prawo", + "fixed_right": "Stała: Prawo" + } + } + } + }, + "zone_thermostat": { + "name": "Termostat", + "state_attributes": { + "preset_mode": { + "state": { + "auto": "Harmonogram (Auto)", + "comfort": "Komfort", + "sleep": "Sen", + "away": "Poza domem" + } + }, + "swing_mode": { + "state": { + "off": "Wyłączony", + "on": "Pełny zakres (Auto)", + "fixed_upper": "Stała: Góra", + "fixed_upper_middle": "Stała: Środek-góra", + "fixed_middle": "Stała: Środek", + "fixed_lower_middle": "Stała: Środek-dół", + "fixed_lower": "Stała: Dół", + "swing_upper": "Ruch: Góra", + "swing_upper_middle": "Ruch: Środek-góra", + "swing_middle": "Ruch: Środek", + "swing_lower_middle": "Ruch: Środek-dół", + "swing_lower": "Ruch: Dół" + } + }, + "swing_horizontal_mode": { + "state": { + "off": "Wyłączony", + "on": "Pełny zakres (Auto)", + "fixed_left": "Stała: Lewo", + "fixed_left_middle": "Stała: Lewo-środek", + "fixed_middle": "Stała: Środek", + "fixed_right_middle": "Stała: Środek-prawo", + "fixed_right": "Stała: Prawo" + } + } + } + } + } } } diff --git a/gree-controller/CHANGELOG.md b/gree-controller/CHANGELOG.md index 0a60f3e..dc331d1 100644 --- a/gree-controller/CHANGELOG.md +++ b/gree-controller/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## 0.15.0 + +- Adds full vertical and horizontal louver positions to manual unit control and thermostat cards, with compact dropdowns that preserve the existing view layout. +- Extends legacy automations, Visual Flow and the bundled Home Assistant climate entities with granular louver positions; legacy boolean swing API payloads remain accepted. +- Direct add-on access no longer shows `Invalid access token` on initial page load; that validation message appears only after a token is submitted. +- Home Assistant climate cards localize direct-control names, fan modes, thermostat presets and all vertical/horizontal louver positions while keeping stable technical mode IDs for services and automations. + ## 0.14.24 - Public Custom Charts fetch chart metadata before language assets, so a chart loads only its selected language pack; missing chart IDs fall back to English. diff --git a/gree-controller/config.yaml b/gree-controller/config.yaml index 4c99ade..9b0608a 100644 --- a/gree-controller/config.yaml +++ b/gree-controller/config.yaml @@ -1,5 +1,5 @@ name: "GREE Controller" -version: "0.14.24" +version: "0.15.0" slug: "gree_controller" description: "Local GREE HVAC controller with Web UI and Home Assistant integration" url: "https://git.linuxiarz.pl/gru/gree-controller-ha-addon/"