Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 15 additions & 4 deletions custom_components/myhome/climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
UnitOfTemperature,
)
from homeassistant.core import HomeAssistant, State, callback
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers.dispatcher import async_dispatcher_connect, async_dispatcher_send
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from OWNd.message import (
Expand Down Expand Up @@ -53,6 +54,7 @@
CONF_HEATING_SUPPORT,
CONF_MANUFACTURER,
CONF_STANDALONE,
DOMAIN,
LOGGER,
signed_who4_temperature,
)
Expand Down Expand Up @@ -482,14 +484,23 @@ async def async_set_fan_mode(self, fan_mode: str) -> None:
}
speed_code = fan_mode_map.get(str(fan_mode).lower())
if speed_code is not None:
self._attr_fan_mode = fan_mode
await self._gateway_handler.send(
OWNHeatingCommand.set_fan_speed(
# OWNd raises for a central unit or general zone: the firmware forwards
# *#4*Z*#11*S## only for a plain zone 1..99 (OWNd#77, A2).
try:
command = OWNHeatingCommand.set_fan_speed(
where=self._where,
speed=speed_code,
standalone=self._standalone,
)
)
except ValueError as err:
raise ServiceValidationError(
f"{self._display_name} has no fan speed of its own: only a zone (1-99) takes one",
translation_domain=DOMAIN,
translation_key="fan_speed_zone_only",
translation_placeholders={"name": self._display_name},
) from err
self._attr_fan_mode = fan_mode
await self._gateway_handler.send(command)
if self.hass is not None:
self.async_write_ha_state()

Expand Down
3 changes: 3 additions & 0 deletions custom_components/myhome/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,9 @@
"alarm_read_only": {
"message": "{name} is read-only: arm and disarm through the AUX frames your central unit is programmed for"
},
"fan_speed_zone_only": {
"message": "{name} has no fan speed of its own: only a zone (1-99) takes one"
},
"command_delivery_cancelled": {
"message": "{name}: direction command delivery was cancelled before reaching the bus"
},
Expand Down
3 changes: 3 additions & 0 deletions custom_components/myhome/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,9 @@
"alarm_read_only": {
"message": "{name} is read-only: arm and disarm through the AUX frames your central unit is programmed for"
},
"fan_speed_zone_only": {
"message": "{name} has no fan speed of its own: only a zone (1-99) takes one"
},
"command_delivery_cancelled": {
"message": "{name}: direction command delivery was cancelled before reaching the bus"
},
Expand Down
3 changes: 3 additions & 0 deletions custom_components/myhome/translations/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,9 @@
"alarm_read_only": {
"message": "{name} est en lecture seule : armez et désarmez via les commandes AUX programmées sur la centrale"
},
"fan_speed_zone_only": {
"message": "{name} n'a pas de vitesse de ventilation propre : seule une zone (1-99) peut en avoir une"
},
"command_delivery_cancelled": {
"message": "{name} : l'envoi de la commande de direction a été annulé avant d'atteindre le bus"
},
Expand Down
3 changes: 3 additions & 0 deletions custom_components/myhome/translations/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,9 @@
"alarm_read_only": {
"message": "{name} è di sola lettura: inserisci e disinserisci l'allarme con i comandi AUX programmati sulla centrale"
},
"fan_speed_zone_only": {
"message": "{name} non ha una velocità del ventilatore propria: solo una zona (1-99) può averne una"
},
"command_delivery_cancelled": {
"message": "{name}: l'invio del comando di direzione è stato annullato prima di raggiungere il bus"
},
Expand Down
3 changes: 3 additions & 0 deletions custom_components/myhome/translations/nl.json
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,9 @@
"alarm_read_only": {
"message": "{name} is alleen-lezen: in- en uitschakelen gebeurt via de AUX-opdrachten waarvoor de centrale is geprogrammeerd"
},
"fan_speed_zone_only": {
"message": "{name} heeft geen eigen ventilatorsnelheid: alleen een zone (1-99) kan er een krijgen"
},
"command_delivery_cancelled": {
"message": "{name}: de aflevering van het richtingscommando is geannuleerd voordat het de bus bereikte"
},
Expand Down
70 changes: 70 additions & 0 deletions tests/test_ownd77_fan_speed_guard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
"""Fan speed on a central unit is refused with a service error (OWNd#77, A2).

The MyHomeServer1 firmware forwards ``*#4*Z*#11*S##`` only for a plain zone 1..99;
``*#4*#0*#11*1##`` and ``*#4*#0#2*#11*1##`` produce no bus frame. OWNd (PR #82)
therefore raises ValueError for a central unit or the general zone. The entity
must turn that into a ServiceValidationError, send nothing and keep its fan mode.
The OWNd builder is patched so the test does not depend on the OWNd release.
"""

from unittest.mock import AsyncMock, MagicMock, patch

import pytest
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ServiceValidationError

from custom_components.myhome.climate import MyHOMEClimate
from custom_components.myhome.const import DOMAIN


@pytest.fixture
def mock_gateway():
gw = MagicMock()
gw.mac = "00:03:50:11:22:77"
gw.log_id = "[OWNd#77]"
gw.send = AsyncMock()
gw.send_status_request = AsyncMock()
return gw


def _central(hass: HomeAssistant, gateway, where: str) -> MyHOMEClimate:
climate = MyHOMEClimate(
hass=hass,
name="Central Unit",
device_id=f"4-{where}",
who="4",
where=where,
heating=True,
cooling=True,
fan=True,
standalone=False,
central=True,
manufacturer="BTicino",
model="Central Unit (3550)",
gateway=gateway,
)
climate.hass = hass
climate.entity_id = "climate.central_unit"
return climate


@pytest.mark.parametrize("where", ["#0", "#0#1"])
async def test_fan_speed_on_central_unit_raises_service_error(
hass: HomeAssistant, mock_gateway, where: str
) -> None:
climate = _central(hass, mock_gateway, where)
before = climate.fan_mode

with (
patch(
"custom_components.myhome.climate.OWNHeatingCommand.set_fan_speed",
side_effect=ValueError(f"Fan speed cannot be set on central unit or general zone: {where}"),
),
pytest.raises(ServiceValidationError) as excinfo,
):
await climate.async_set_fan_mode("high")

assert excinfo.value.translation_domain == DOMAIN
assert excinfo.value.translation_key == "fan_speed_zone_only"
mock_gateway.send.assert_not_awaited()
assert climate.fan_mode == before
Loading