Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
33 changes: 27 additions & 6 deletions homeassistant/components/zhong_hong/climate.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Support for ZhongHong HVAC Controller."""

from datetime import timedelta
import logging
from typing import Any, override

Expand Down Expand Up @@ -40,6 +41,7 @@

DEFAULT_PORT = 9999
DEFAULT_GATEWAY_ADDRRESS = 1
SCAN_INTERVAL = timedelta(seconds=60)

SIGNAL_DEVICE_ADDED = "zhong_hong_device_added"
SIGNAL_ZHONG_HONG_HUB_START = "zhong_hong_hub_start"
Expand Down Expand Up @@ -140,7 +142,7 @@ class ZhongHongClimate(ClimateEntity):
HVACMode.FAN_ONLY,
HVACMode.OFF,
]
_attr_should_poll = False
_attr_should_poll = True
_attr_supported_features = (
ClimateEntityFeature.TARGET_TEMPERATURE
| ClimateEntityFeature.FAN_MODE
Expand Down Expand Up @@ -183,6 +185,17 @@ def _after_update(self, climate):
self._attr_target_temperature = self._device.target_temperature
self.schedule_update_ha_state()

def update(self) -> None:
"""Poll the gateway for fresh status when the connection is healthy."""
if self._hub.connected:
self._device.update()

@property
@override
def available(self) -> bool:
"""Return False when the gateway connection is unhealthy."""
return self._hub.connected

@property
@override
def hvac_mode(self) -> HVACMode:
Expand Down Expand Up @@ -227,18 +240,23 @@ def max_temp(self) -> float:
@override
def turn_on(self) -> None:
"""Turn on ac."""
return self._device.turn_on()
if not self._device.turn_on():
_LOGGER.warning("%s: failed to send turn-on command", self.entity_id)
Comment on lines +243 to +244

@override
def turn_off(self) -> None:
"""Turn off ac."""
return self._device.turn_off()
if not self._device.turn_off():
_LOGGER.warning("%s: failed to send turn-off command", self.entity_id)

@override
def set_temperature(self, **kwargs: Any) -> None:
"""Set new target temperature."""
if (temperature := kwargs.get(ATTR_TEMPERATURE)) is not None:
self._device.set_temperature(temperature)
if not self._device.set_temperature(temperature):
_LOGGER.warning(
"%s: failed to send temperature command", self.entity_id
)

if (operation_mode := kwargs.get(ATTR_HVAC_MODE)) is not None:
self.set_hvac_mode(operation_mode)
Expand All @@ -254,12 +272,15 @@ def set_hvac_mode(self, hvac_mode: HVACMode) -> None:
if not self.is_on:
self.turn_on()

self._device.set_operation_mode(hvac_mode.upper())
if not self._device.set_operation_mode(hvac_mode.upper()):
_LOGGER.warning("%s: failed to send mode command", self.entity_id)

@override
def set_fan_mode(self, fan_mode: str) -> None:
"""Set new target fan mode."""
mapped_mode = FAN_MODE_MAP.get(fan_mode)
if not mapped_mode:
_LOGGER.error("Unsupported fan mode: %s", fan_mode)
self._device.set_fan_mode(mapped_mode)
return
if not self._device.set_fan_mode(mapped_mode):
_LOGGER.warning("%s: failed to send fan command", self.entity_id)
2 changes: 1 addition & 1 deletion homeassistant/components/zhong_hong/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@
"iot_class": "local_push",
"loggers": ["zhong_hong_hvac"],
"quality_scale": "legacy",
"requirements": ["zhong-hong-hvac==1.0.13"]
"requirements": ["zhong-hong-hvac==1.0.15"]
}
2 changes: 1 addition & 1 deletion requirements_all.txt

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions tests/components/zhong_hong/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""zhong_hong tests."""
291 changes: 291 additions & 0 deletions tests/components/zhong_hong/test_climate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,291 @@
"""Test the zhong_hong climate platform."""

from unittest.mock import patch

import pytest

from homeassistant.components.climate import (
ATTR_FAN_MODE,
ATTR_HVAC_MODE,
DOMAIN as CLIMATE_DOMAIN,
FAN_LOW,
SERVICE_SET_FAN_MODE,
SERVICE_SET_HVAC_MODE,
SERVICE_SET_TEMPERATURE,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
HVACMode,
)
from homeassistant.const import (
ATTR_ENTITY_ID,
ATTR_TEMPERATURE,
STATE_OFF,
STATE_UNAVAILABLE,
)
from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component

ENTITY_ID = "climate.zhong_hong_hvac_1_1"
HOST = "1.2.3.4"


class FakeGateway:
"""Test double for the zhong_hong_hvac gateway."""

def __init__(self) -> None:
"""Initialize the fake gateway."""
self.connected = True
self.send_result = True
self.query_status_calls = 0

@property
def gw_addr(self) -> int:
"""Return the gateway address."""
return 1

def add_status_callback(self, ac_addr, callback) -> None:
"""Register a status callback (no-op)."""

def add_device(self, device) -> None:
"""Register a device (no-op)."""

def discovery_ac(self) -> list[tuple[int, int]]:
"""Return the discovered device addresses."""
return [(1, 1)]

def start_listen(self) -> None:
"""Start listening (no-op)."""

def query_all_status(self) -> None:
"""Query all devices (no-op)."""

def stop_listen(self) -> None:
"""Stop listening (no-op)."""

def query_status(self, ac_addr) -> bool:
"""Query the status of a device."""
self.query_status_calls += 1
return self.send_result

def send(self, ac_data) -> bool:
"""Send a command to the gateway."""
return self.send_result


@pytest.fixture
def gateway() -> FakeGateway:
"""Return a fake gateway."""
return FakeGateway()


async def _setup_climate(hass: HomeAssistant, gateway: FakeGateway) -> None:
"""Set up the zhong_hong climate platform with a fake gateway."""
with patch(
"homeassistant.components.zhong_hong.climate.ZhongHongGateway",
return_value=gateway,
):
assert await async_setup_component(
hass,
CLIMATE_DOMAIN,
{CLIMATE_DOMAIN: {"platform": "zhong_hong", "host": HOST}},
)
await hass.async_block_till_done()


async def test_setup_creates_entity(hass: HomeAssistant, gateway: FakeGateway) -> None:
"""Test the entity is created and polling is enabled."""
await _setup_climate(hass, gateway)

state = hass.states.get(ENTITY_ID)
assert state is not None
assert state.state == STATE_OFF

entity = hass.data[CLIMATE_DOMAIN].get_entity(ENTITY_ID)
assert entity is not None
assert entity.should_poll is True


async def test_unavailable_when_gateway_disconnected(
hass: HomeAssistant, gateway: FakeGateway
) -> None:
"""Test the entity is unavailable when the gateway connection is unhealthy."""
gateway.connected = False
await _setup_climate(hass, gateway)

state = hass.states.get(ENTITY_ID)
assert state is not None
assert state.state == STATE_UNAVAILABLE


async def test_update_queries_gateway_when_connected(
hass: HomeAssistant, gateway: FakeGateway
) -> None:
"""Test update polls the gateway when the connection is healthy."""
await _setup_climate(hass, gateway)

entity = hass.data[CLIMATE_DOMAIN].get_entity(ENTITY_ID)
assert entity is not None
await entity.async_device_update()

assert gateway.query_status_calls == 1


async def test_update_skips_gateway_when_disconnected(
hass: HomeAssistant, gateway: FakeGateway
) -> None:
"""Test update does not poll the gateway when the connection is unhealthy."""
gateway.connected = False
await _setup_climate(hass, gateway)

entity = hass.data[CLIMATE_DOMAIN].get_entity(ENTITY_ID)
assert entity is not None
await entity.async_device_update()

assert gateway.query_status_calls == 0


async def test_turn_on_success(
hass: HomeAssistant, gateway: FakeGateway, caplog: pytest.LogCaptureFixture
) -> None:
"""Test turn_on does not log a warning when the command is sent."""
await _setup_climate(hass, gateway)

await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: ENTITY_ID},
blocking=True,
)

assert "failed to send" not in caplog.text


async def test_turn_on_send_failure_logs_warning(
hass: HomeAssistant, gateway: FakeGateway, caplog: pytest.LogCaptureFixture
) -> None:
"""Test turn_on logs a warning when the command cannot be sent."""
gateway.send_result = False
await _setup_climate(hass, gateway)

await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: ENTITY_ID},
blocking=True,
)

assert "failed to send turn-on command" in caplog.text


async def test_turn_off_send_failure_logs_warning(
hass: HomeAssistant, gateway: FakeGateway, caplog: pytest.LogCaptureFixture
) -> None:
"""Test turn_off logs a warning when the command cannot be sent."""
gateway.send_result = False
await _setup_climate(hass, gateway)

await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_TURN_OFF,
{ATTR_ENTITY_ID: ENTITY_ID},
blocking=True,
)

assert "failed to send turn-off command" in caplog.text


async def test_set_temperature_send_failure_logs_warning(
hass: HomeAssistant, gateway: FakeGateway, caplog: pytest.LogCaptureFixture
) -> None:
"""Test set_temperature logs a warning when the command cannot be sent."""
gateway.send_result = False
await _setup_climate(hass, gateway)

await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_TEMPERATURE,
{ATTR_ENTITY_ID: ENTITY_ID, ATTR_TEMPERATURE: 25},
blocking=True,
)

assert "failed to send temperature command" in caplog.text


async def test_set_hvac_mode_send_failure_logs_warning(
hass: HomeAssistant, gateway: FakeGateway, caplog: pytest.LogCaptureFixture
) -> None:
"""Test set_hvac_mode logs a warning when the command cannot be sent."""
gateway.send_result = False
await _setup_climate(hass, gateway)

await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_HVAC_MODE,
{ATTR_ENTITY_ID: ENTITY_ID, ATTR_HVAC_MODE: HVACMode.COOL},
blocking=True,
)

assert "failed to send mode command" in caplog.text


async def test_set_hvac_mode_success(
hass: HomeAssistant, gateway: FakeGateway, caplog: pytest.LogCaptureFixture
) -> None:
"""Test set_hvac_mode does not log a warning when the command is sent."""
await _setup_climate(hass, gateway)

await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_HVAC_MODE,
{ATTR_ENTITY_ID: ENTITY_ID, ATTR_HVAC_MODE: HVACMode.COOL},
blocking=True,
)

assert "failed to send" not in caplog.text


async def test_set_fan_mode_unsupported_logs_error(
hass: HomeAssistant, gateway: FakeGateway, caplog: pytest.LogCaptureFixture
) -> None:
"""Test set_fan_mode with an unsupported mode logs an error and sends nothing."""
await _setup_climate(hass, gateway)

entity = hass.data[CLIMATE_DOMAIN].get_entity(ENTITY_ID)
assert entity is not None
await entity.async_set_fan_mode("unknown")

assert "Unsupported fan mode: unknown" in caplog.text
assert "failed to send" not in caplog.text


async def test_set_fan_mode_send_failure_logs_warning(
hass: HomeAssistant, gateway: FakeGateway, caplog: pytest.LogCaptureFixture
) -> None:
"""Test set_fan_mode logs a warning when the command cannot be sent."""
gateway.send_result = False
await _setup_climate(hass, gateway)

await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_FAN_MODE,
{ATTR_ENTITY_ID: ENTITY_ID, ATTR_FAN_MODE: FAN_LOW},
blocking=True,
)

assert "failed to send fan command" in caplog.text


async def test_set_fan_mode_success(
hass: HomeAssistant, gateway: FakeGateway, caplog: pytest.LogCaptureFixture
) -> None:
"""Test set_fan_mode does not log a warning when the command is sent."""
await _setup_climate(hass, gateway)

await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_FAN_MODE,
{ATTR_ENTITY_ID: ENTITY_ID, ATTR_FAN_MODE: FAN_LOW},
blocking=True,
)

assert "failed to send" not in caplog.text
Loading