mirror of
https://github.com/home-assistant/core.git
synced 2026-09-03 12:02:16 +01:00
Bump pyIntesishome to 2.5.0 (#179979)
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
b7022436a6
commit
6ab47ecb74
@@ -1,7 +1,6 @@
|
||||
"""Support for IntesisHome and airconwithme Smart AC Controllers."""
|
||||
|
||||
import logging
|
||||
from random import randrange
|
||||
from typing import Any, NamedTuple, override
|
||||
|
||||
from pyintesishome import IHAuthenticationError, IHConnectionError, IntesisHome
|
||||
@@ -26,14 +25,14 @@ from homeassistant.const import (
|
||||
CONF_DEVICE,
|
||||
CONF_PASSWORD,
|
||||
CONF_USERNAME,
|
||||
EVENT_HOMEASSISTANT_STOP,
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import Event, HomeAssistant
|
||||
from homeassistant.exceptions import PlatformNotReady
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.event import async_call_later
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -115,7 +114,7 @@ async def async_setup_platform(
|
||||
device_type=device_type,
|
||||
)
|
||||
try:
|
||||
await controller.poll_status()
|
||||
await controller.connect()
|
||||
except IHAuthenticationError:
|
||||
_LOGGER.error("Invalid username or password")
|
||||
return
|
||||
@@ -124,6 +123,12 @@ async def async_setup_platform(
|
||||
raise PlatformNotReady from ex
|
||||
|
||||
if ih_devices := controller.get_devices():
|
||||
# The controller is shared by every entity, so it outlives any one of
|
||||
# them and is only torn down with Home Assistant itself.
|
||||
async def _async_stop_controller(event: Event) -> None:
|
||||
await controller.stop()
|
||||
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_stop_controller)
|
||||
async_add_entities(
|
||||
[
|
||||
IntesisAC(ih_device_id, device, controller)
|
||||
@@ -144,7 +149,6 @@ class IntesisAC(ClimateEntity):
|
||||
"""Represents an Intesishome air conditioning device."""
|
||||
|
||||
_attr_preset_modes = [PRESET_ECO, PRESET_COMFORT, PRESET_BOOST]
|
||||
_attr_should_poll = False
|
||||
_attr_target_temperature_step = 1
|
||||
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||
|
||||
@@ -155,7 +159,6 @@ class IntesisAC(ClimateEntity):
|
||||
self._ih_device = ih_device
|
||||
self._attr_name = ih_device.get("name")
|
||||
self._device_type = controller.device_type
|
||||
self._connected = None
|
||||
self._attr_hvac_modes = []
|
||||
self._outdoor_temp = None
|
||||
self._hvac_mode = None
|
||||
@@ -210,11 +213,11 @@ class IntesisAC(ClimateEntity):
|
||||
"""Subscribe to event updates."""
|
||||
_LOGGER.debug("Added climate device with state: %s", repr(self._ih_device))
|
||||
self._controller.add_update_callback(self.async_update_callback)
|
||||
try:
|
||||
await self._controller.connect()
|
||||
except IHConnectionError as ex:
|
||||
_LOGGER.error("Exception connecting to IntesisHome: %s", ex)
|
||||
raise PlatformNotReady from ex
|
||||
|
||||
@override
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
"""Unsubscribe from event updates."""
|
||||
self._controller.remove_update_callback(self.async_update_callback)
|
||||
|
||||
@property
|
||||
@override
|
||||
@@ -313,7 +316,6 @@ class IntesisAC(ClimateEntity):
|
||||
async def async_update(self) -> None:
|
||||
"""Copy values from controller dictionary to climate device."""
|
||||
# Update values from controller's device dictionary
|
||||
self._connected = self._controller.is_connected
|
||||
self._attr_current_temperature = self._controller.get_temperature(
|
||||
self._device_id
|
||||
)
|
||||
@@ -347,11 +349,6 @@ class IntesisAC(ClimateEntity):
|
||||
self._device_id
|
||||
)
|
||||
|
||||
@override
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
"""Shutdown the controller when the device is being removed."""
|
||||
await self._controller.stop()
|
||||
|
||||
@property
|
||||
@override
|
||||
def icon(self) -> str | None:
|
||||
@@ -362,28 +359,6 @@ class IntesisAC(ClimateEntity):
|
||||
|
||||
async def async_update_callback(self, device_id=None):
|
||||
"""Let HA know there has been an update from the controller."""
|
||||
# Track changes in connection state
|
||||
if not self._controller.is_connected and self._connected:
|
||||
# Connection has dropped
|
||||
self._connected = False
|
||||
reconnect_minutes = 1 + randrange(10)
|
||||
_LOGGER.error(
|
||||
"Connection to %s API was lost. Reconnecting in %i minutes",
|
||||
self._device_type,
|
||||
reconnect_minutes,
|
||||
)
|
||||
# Schedule reconnection
|
||||
|
||||
async def try_connect(_now):
|
||||
await self._controller.connect()
|
||||
|
||||
async_call_later(self.hass, reconnect_minutes * 60, try_connect)
|
||||
|
||||
if self._controller.is_connected and not self._connected:
|
||||
# Connection has been restored
|
||||
self._connected = True
|
||||
_LOGGER.debug("Connection to %s API was restored", self._device_type)
|
||||
|
||||
if not device_id or self._device_id == device_id:
|
||||
# Update all devices if no device_id was specified
|
||||
_LOGGER.debug(
|
||||
@@ -410,8 +385,8 @@ class IntesisAC(ClimateEntity):
|
||||
@property
|
||||
@override
|
||||
def available(self) -> bool:
|
||||
"""If the device hasn't been able to connect, mark as unavailable."""
|
||||
return self._connected or self._connected is None
|
||||
"""Return whether the controller still has a path to the device."""
|
||||
return self._controller.is_available
|
||||
|
||||
@property
|
||||
@override
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
"name": "IntesisHome",
|
||||
"codeowners": ["@jnimmo"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/intesishome",
|
||||
"iot_class": "cloud_push",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pyintesishome"],
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["pyintesishome==2.0.3"]
|
||||
"requirements": ["pyintesishome==2.5.0"]
|
||||
}
|
||||
|
||||
@@ -3373,7 +3373,7 @@
|
||||
"name": "IntesisHome",
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "cloud_push"
|
||||
"iot_class": "cloud_polling"
|
||||
},
|
||||
"iometer": {
|
||||
"name": "IOmeter",
|
||||
|
||||
Generated
+1
-1
@@ -2276,7 +2276,7 @@ pyinsteon==1.6.4
|
||||
pyintelliclima==0.4.1
|
||||
|
||||
# homeassistant.components.intesishome
|
||||
pyintesishome==2.0.3
|
||||
pyintesishome==2.5.0
|
||||
|
||||
# homeassistant.components.ipma
|
||||
pyipma==3.0.10
|
||||
|
||||
@@ -1,15 +1,33 @@
|
||||
"""Tests for the IntesisHome climate platform."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from datetime import timedelta
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_PLATFORM, CONF_USERNAME
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
from pyintesishome import IHConnectionError
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN, SCAN_INTERVAL
|
||||
from homeassistant.const import (
|
||||
CONF_PASSWORD,
|
||||
CONF_PLATFORM,
|
||||
CONF_USERNAME,
|
||||
STATE_UNAVAILABLE,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.entity_platform import PLATFORM_NOT_READY_BASE_WAIT_TIME
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from tests.common import async_fire_time_changed
|
||||
|
||||
async def test_setup_platform_registers_callback(hass: HomeAssistant) -> None:
|
||||
"""Test registering the synchronous library update callback during setup."""
|
||||
ENTITY_ID = "climate.office"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_controller() -> Generator[MagicMock]:
|
||||
"""Mock the pyintesishome controller."""
|
||||
with patch(
|
||||
"homeassistant.components.intesishome.climate.IntesisHome", autospec=True
|
||||
) as intesis_home:
|
||||
@@ -22,7 +40,8 @@ async def test_setup_platform_registers_callback(hass: HomeAssistant) -> None:
|
||||
controller.get_fan_speed_list.return_value = []
|
||||
controller.get_mode_list.return_value = []
|
||||
controller.add_update_callback = MagicMock()
|
||||
controller.is_connected = True
|
||||
controller.remove_update_callback = MagicMock()
|
||||
controller.is_available = True
|
||||
controller.get_temperature.return_value = 22
|
||||
controller.get_fan_speed.return_value = None
|
||||
controller.is_on.return_value = False
|
||||
@@ -38,21 +57,106 @@ async def test_setup_platform_registers_callback(hass: HomeAssistant) -> None:
|
||||
controller.get_horizontal_swing.return_value = "auto/stop"
|
||||
controller.get_heat_power_consumption.return_value = None
|
||||
controller.get_cool_power_consumption.return_value = None
|
||||
yield controller
|
||||
|
||||
assert await async_setup_component(
|
||||
hass,
|
||||
CLIMATE_DOMAIN,
|
||||
{
|
||||
CLIMATE_DOMAIN: {
|
||||
CONF_PLATFORM: "intesishome",
|
||||
CONF_USERNAME: "user",
|
||||
CONF_PASSWORD: "password",
|
||||
}
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert hass.states.get("climate.office") is not None
|
||||
controller.add_update_callback.assert_called_once()
|
||||
assert callable(controller.add_update_callback.call_args.args[0])
|
||||
controller.connect.assert_awaited_once_with()
|
||||
async def setup_platform(hass: HomeAssistant) -> None:
|
||||
"""Set up the IntesisHome climate platform."""
|
||||
assert await async_setup_component(
|
||||
hass,
|
||||
CLIMATE_DOMAIN,
|
||||
{
|
||||
CLIMATE_DOMAIN: {
|
||||
CONF_PLATFORM: "intesishome",
|
||||
CONF_USERNAME: "user",
|
||||
CONF_PASSWORD: "password",
|
||||
}
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
||||
async def test_setup_platform_registers_callback(
|
||||
hass: HomeAssistant, mock_controller: MagicMock
|
||||
) -> None:
|
||||
"""Test registering the synchronous library update callback during setup."""
|
||||
await setup_platform(hass)
|
||||
|
||||
assert hass.states.get(ENTITY_ID) is not None
|
||||
mock_controller.add_update_callback.assert_called_once()
|
||||
assert callable(mock_controller.add_update_callback.call_args.args[0])
|
||||
mock_controller.connect.assert_awaited_once_with()
|
||||
|
||||
|
||||
async def test_availability_follows_controller(
|
||||
hass: HomeAssistant, mock_controller: MagicMock, freezer: FrozenDateTimeFactory
|
||||
) -> None:
|
||||
"""Test polling picks up availability changes with no library callback."""
|
||||
await setup_platform(hass)
|
||||
assert hass.states.get(ENTITY_ID).state != STATE_UNAVAILABLE
|
||||
|
||||
# Availability follows how long since the controller's poller last got
|
||||
# through, so nothing calls back to announce either transition.
|
||||
mock_controller.is_available = False
|
||||
freezer.tick(SCAN_INTERVAL)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
assert hass.states.get(ENTITY_ID).state == STATE_UNAVAILABLE
|
||||
|
||||
mock_controller.is_available = True
|
||||
freezer.tick(SCAN_INTERVAL)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
assert hass.states.get(ENTITY_ID).state != STATE_UNAVAILABLE
|
||||
|
||||
# Recovered without reconnecting: connect() was only called during setup.
|
||||
assert mock_controller.connect.await_count == 1
|
||||
|
||||
|
||||
async def test_setup_platform_retries_on_connection_error(
|
||||
hass: HomeAssistant, mock_controller: MagicMock, freezer: FrozenDateTimeFactory
|
||||
) -> None:
|
||||
"""Test an unreachable API leaves the platform to be set up again later."""
|
||||
mock_controller.connect.side_effect = IHConnectionError
|
||||
|
||||
await setup_platform(hass)
|
||||
assert hass.states.get(ENTITY_ID) is None
|
||||
|
||||
mock_controller.connect.side_effect = None
|
||||
freezer.tick(timedelta(seconds=PLATFORM_NOT_READY_BASE_WAIT_TIME))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert hass.states.get(ENTITY_ID) is not None
|
||||
|
||||
|
||||
async def test_removing_one_entity_keeps_controller_running(
|
||||
hass: HomeAssistant,
|
||||
mock_controller: MagicMock,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test the shared controller outlives the removal of a single entity."""
|
||||
mock_controller.get_devices.return_value = {
|
||||
"device-id": {"name": "Office"},
|
||||
"other-device-id": {"name": "Lounge"},
|
||||
}
|
||||
await setup_platform(hass)
|
||||
|
||||
registered_callbacks = [
|
||||
call.args[0] for call in mock_controller.add_update_callback.call_args_list
|
||||
]
|
||||
assert len(registered_callbacks) == 2
|
||||
|
||||
entity_registry.async_remove(ENTITY_ID)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert hass.states.get(ENTITY_ID) is None
|
||||
mock_controller.stop.assert_not_awaited()
|
||||
assert hass.states.get("climate.lounge").state != STATE_UNAVAILABLE
|
||||
|
||||
# Only the removed entity detaches; the other keeps receiving updates.
|
||||
removed_callbacks = [
|
||||
call.args[0] for call in mock_controller.remove_update_callback.call_args_list
|
||||
]
|
||||
assert len(removed_callbacks) == 1
|
||||
assert removed_callbacks[0] in registered_callbacks
|
||||
|
||||
Reference in New Issue
Block a user