mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 11:17:21 +00:00
Add A. O. Smith integration (#104976)
This commit is contained in:
parent
fdeb9e36c3
commit
1c7bd3f729
@ -86,6 +86,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/anova/ @Lash-L
|
/tests/components/anova/ @Lash-L
|
||||||
/homeassistant/components/anthemav/ @hyralex
|
/homeassistant/components/anthemav/ @hyralex
|
||||||
/tests/components/anthemav/ @hyralex
|
/tests/components/anthemav/ @hyralex
|
||||||
|
/homeassistant/components/aosmith/ @bdr99
|
||||||
|
/tests/components/aosmith/ @bdr99
|
||||||
/homeassistant/components/apache_kafka/ @bachya
|
/homeassistant/components/apache_kafka/ @bachya
|
||||||
/tests/components/apache_kafka/ @bachya
|
/tests/components/apache_kafka/ @bachya
|
||||||
/homeassistant/components/apcupsd/ @yuxincs
|
/homeassistant/components/apcupsd/ @yuxincs
|
||||||
|
53
homeassistant/components/aosmith/__init__.py
Normal file
53
homeassistant/components/aosmith/__init__.py
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
"""The A. O. Smith integration."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from py_aosmith import AOSmithAPIClient
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, Platform
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers import aiohttp_client
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
|
from .coordinator import AOSmithCoordinator
|
||||||
|
|
||||||
|
PLATFORMS: list[Platform] = [Platform.WATER_HEATER]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class AOSmithData:
|
||||||
|
"""Data for the A. O. Smith integration."""
|
||||||
|
|
||||||
|
coordinator: AOSmithCoordinator
|
||||||
|
client: AOSmithAPIClient
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
|
"""Set up A. O. Smith from a config entry."""
|
||||||
|
email = entry.data[CONF_EMAIL]
|
||||||
|
password = entry.data[CONF_PASSWORD]
|
||||||
|
|
||||||
|
session = aiohttp_client.async_get_clientsession(hass)
|
||||||
|
client = AOSmithAPIClient(email, password, session)
|
||||||
|
coordinator = AOSmithCoordinator(hass, client)
|
||||||
|
|
||||||
|
# Fetch initial data so we have data when entities subscribe
|
||||||
|
await coordinator.async_config_entry_first_refresh()
|
||||||
|
|
||||||
|
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = AOSmithData(
|
||||||
|
coordinator=coordinator, client=client
|
||||||
|
)
|
||||||
|
|
||||||
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
|
"""Unload a config entry."""
|
||||||
|
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||||
|
hass.data[DOMAIN].pop(entry.entry_id)
|
||||||
|
|
||||||
|
return unload_ok
|
61
homeassistant/components/aosmith/config_flow.py
Normal file
61
homeassistant/components/aosmith/config_flow.py
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
"""Config flow for A. O. Smith integration."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from py_aosmith import AOSmithAPIClient, AOSmithInvalidCredentialsException
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant import config_entries
|
||||||
|
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
|
||||||
|
from homeassistant.data_entry_flow import FlowResult
|
||||||
|
from homeassistant.helpers import aiohttp_client
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||||
|
"""Handle a config flow for A. O. Smith."""
|
||||||
|
|
||||||
|
VERSION = 1
|
||||||
|
|
||||||
|
async def async_step_user(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> FlowResult:
|
||||||
|
"""Handle the initial step."""
|
||||||
|
errors: dict[str, str] = {}
|
||||||
|
if user_input is not None:
|
||||||
|
unique_id = user_input[CONF_EMAIL].lower()
|
||||||
|
await self.async_set_unique_id(unique_id)
|
||||||
|
self._abort_if_unique_id_configured()
|
||||||
|
|
||||||
|
session = aiohttp_client.async_get_clientsession(self.hass)
|
||||||
|
client = AOSmithAPIClient(
|
||||||
|
user_input[CONF_EMAIL], user_input[CONF_PASSWORD], session
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
await client.get_devices()
|
||||||
|
except AOSmithInvalidCredentialsException:
|
||||||
|
errors["base"] = "invalid_auth"
|
||||||
|
except Exception: # pylint: disable=broad-except
|
||||||
|
_LOGGER.exception("Unexpected exception")
|
||||||
|
errors["base"] = "unknown"
|
||||||
|
else:
|
||||||
|
return self.async_create_entry(
|
||||||
|
title=user_input[CONF_EMAIL], data=user_input
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="user",
|
||||||
|
data_schema=vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required(CONF_EMAIL): str,
|
||||||
|
vol.Required(CONF_PASSWORD): str,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
errors=errors,
|
||||||
|
)
|
16
homeassistant/components/aosmith/const.py
Normal file
16
homeassistant/components/aosmith/const.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
"""Constants for the A. O. Smith integration."""
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
DOMAIN = "aosmith"
|
||||||
|
|
||||||
|
AOSMITH_MODE_ELECTRIC = "ELECTRIC"
|
||||||
|
AOSMITH_MODE_HEAT_PUMP = "HEAT_PUMP"
|
||||||
|
AOSMITH_MODE_HYBRID = "HYBRID"
|
||||||
|
AOSMITH_MODE_VACATION = "VACATION"
|
||||||
|
|
||||||
|
# Update interval to be used for normal background updates.
|
||||||
|
REGULAR_INTERVAL = timedelta(seconds=30)
|
||||||
|
|
||||||
|
# Update interval to be used while a mode or setpoint change is in progress.
|
||||||
|
FAST_INTERVAL = timedelta(seconds=1)
|
48
homeassistant/components/aosmith/coordinator.py
Normal file
48
homeassistant/components/aosmith/coordinator.py
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
"""The data update coordinator for the A. O. Smith integration."""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from py_aosmith import (
|
||||||
|
AOSmithAPIClient,
|
||||||
|
AOSmithInvalidCredentialsException,
|
||||||
|
AOSmithUnknownException,
|
||||||
|
)
|
||||||
|
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
|
|
||||||
|
from .const import DOMAIN, FAST_INTERVAL, REGULAR_INTERVAL
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class AOSmithCoordinator(DataUpdateCoordinator[dict[str, dict[str, Any]]]):
|
||||||
|
"""Custom data update coordinator for A. O. Smith integration."""
|
||||||
|
|
||||||
|
def __init__(self, hass: HomeAssistant, client: AOSmithAPIClient) -> None:
|
||||||
|
"""Initialize the coordinator."""
|
||||||
|
super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=REGULAR_INTERVAL)
|
||||||
|
self.client = client
|
||||||
|
|
||||||
|
async def _async_update_data(self) -> dict[str, dict[str, Any]]:
|
||||||
|
"""Fetch latest data from API."""
|
||||||
|
try:
|
||||||
|
devices = await self.client.get_devices()
|
||||||
|
except (AOSmithInvalidCredentialsException, AOSmithUnknownException) as err:
|
||||||
|
raise UpdateFailed(f"Error communicating with API: {err}") from err
|
||||||
|
|
||||||
|
mode_pending = any(
|
||||||
|
device.get("data", {}).get("modePending") for device in devices
|
||||||
|
)
|
||||||
|
setpoint_pending = any(
|
||||||
|
device.get("data", {}).get("temperatureSetpointPending")
|
||||||
|
for device in devices
|
||||||
|
)
|
||||||
|
|
||||||
|
if mode_pending or setpoint_pending:
|
||||||
|
self.update_interval = FAST_INTERVAL
|
||||||
|
else:
|
||||||
|
self.update_interval = REGULAR_INTERVAL
|
||||||
|
|
||||||
|
return {device.get("junctionId"): device for device in devices}
|
51
homeassistant/components/aosmith/entity.py
Normal file
51
homeassistant/components/aosmith/entity.py
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
"""The base entity for the A. O. Smith integration."""
|
||||||
|
|
||||||
|
|
||||||
|
from py_aosmith import AOSmithAPIClient
|
||||||
|
|
||||||
|
from homeassistant.helpers.device_registry import DeviceInfo
|
||||||
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
|
from .coordinator import AOSmithCoordinator
|
||||||
|
|
||||||
|
|
||||||
|
class AOSmithEntity(CoordinatorEntity[AOSmithCoordinator]):
|
||||||
|
"""Base entity for A. O. Smith."""
|
||||||
|
|
||||||
|
_attr_has_entity_name = True
|
||||||
|
|
||||||
|
def __init__(self, coordinator: AOSmithCoordinator, junction_id: str) -> None:
|
||||||
|
"""Initialize the entity."""
|
||||||
|
super().__init__(coordinator)
|
||||||
|
self.junction_id = junction_id
|
||||||
|
self._attr_device_info = DeviceInfo(
|
||||||
|
manufacturer="A. O. Smith",
|
||||||
|
name=self.device.get("name"),
|
||||||
|
model=self.device.get("model"),
|
||||||
|
serial_number=self.device.get("serial"),
|
||||||
|
suggested_area=self.device.get("install", {}).get("location"),
|
||||||
|
identifiers={(DOMAIN, junction_id)},
|
||||||
|
sw_version=self.device.get("data", {}).get("firmwareVersion"),
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device(self):
|
||||||
|
"""Shortcut to get the device status from the coordinator data."""
|
||||||
|
return self.coordinator.data.get(self.junction_id)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_data(self):
|
||||||
|
"""Shortcut to get the device data within the device status."""
|
||||||
|
device = self.device
|
||||||
|
return None if device is None else device.get("data", {})
|
||||||
|
|
||||||
|
@property
|
||||||
|
def client(self) -> AOSmithAPIClient:
|
||||||
|
"""Shortcut to get the API client."""
|
||||||
|
return self.coordinator.client
|
||||||
|
|
||||||
|
@property
|
||||||
|
def available(self) -> bool:
|
||||||
|
"""Return True if entity is available."""
|
||||||
|
return super().available and self.device_data.get("isOnline") is True
|
10
homeassistant/components/aosmith/manifest.json
Normal file
10
homeassistant/components/aosmith/manifest.json
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"domain": "aosmith",
|
||||||
|
"name": "A. O. Smith",
|
||||||
|
"codeowners": ["@bdr99"],
|
||||||
|
"config_flow": true,
|
||||||
|
"documentation": "https://www.home-assistant.io/integrations/aosmith",
|
||||||
|
"iot_class": "cloud_polling",
|
||||||
|
"quality_scale": "platinum",
|
||||||
|
"requirements": ["py-aosmith==1.0.1"]
|
||||||
|
}
|
20
homeassistant/components/aosmith/strings.json
Normal file
20
homeassistant/components/aosmith/strings.json
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"email": "[%key:common::config_flow::data::email%]",
|
||||||
|
"password": "[%key:common::config_flow::data::password%]"
|
||||||
|
},
|
||||||
|
"description": "Please enter your A. O. Smith credentials."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||||
|
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||||
|
},
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
149
homeassistant/components/aosmith/water_heater.py
Normal file
149
homeassistant/components/aosmith/water_heater.py
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
"""The water heater platform for the A. O. Smith integration."""
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from homeassistant.components.water_heater import (
|
||||||
|
STATE_ECO,
|
||||||
|
STATE_ELECTRIC,
|
||||||
|
STATE_HEAT_PUMP,
|
||||||
|
STATE_OFF,
|
||||||
|
WaterHeaterEntity,
|
||||||
|
WaterHeaterEntityFeature,
|
||||||
|
)
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.const import UnitOfTemperature
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
|
from . import AOSmithData
|
||||||
|
from .const import (
|
||||||
|
AOSMITH_MODE_ELECTRIC,
|
||||||
|
AOSMITH_MODE_HEAT_PUMP,
|
||||||
|
AOSMITH_MODE_HYBRID,
|
||||||
|
AOSMITH_MODE_VACATION,
|
||||||
|
DOMAIN,
|
||||||
|
)
|
||||||
|
from .coordinator import AOSmithCoordinator
|
||||||
|
from .entity import AOSmithEntity
|
||||||
|
|
||||||
|
MODE_HA_TO_AOSMITH = {
|
||||||
|
STATE_OFF: AOSMITH_MODE_VACATION,
|
||||||
|
STATE_ECO: AOSMITH_MODE_HYBRID,
|
||||||
|
STATE_ELECTRIC: AOSMITH_MODE_ELECTRIC,
|
||||||
|
STATE_HEAT_PUMP: AOSMITH_MODE_HEAT_PUMP,
|
||||||
|
}
|
||||||
|
MODE_AOSMITH_TO_HA = {
|
||||||
|
AOSMITH_MODE_ELECTRIC: STATE_ELECTRIC,
|
||||||
|
AOSMITH_MODE_HEAT_PUMP: STATE_HEAT_PUMP,
|
||||||
|
AOSMITH_MODE_HYBRID: STATE_ECO,
|
||||||
|
AOSMITH_MODE_VACATION: STATE_OFF,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Operation mode to use when exiting away mode
|
||||||
|
DEFAULT_OPERATION_MODE = AOSMITH_MODE_HYBRID
|
||||||
|
|
||||||
|
DEFAULT_SUPPORT_FLAGS = (
|
||||||
|
WaterHeaterEntityFeature.TARGET_TEMPERATURE
|
||||||
|
| WaterHeaterEntityFeature.OPERATION_MODE
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||||
|
) -> None:
|
||||||
|
"""Set up A. O. Smith water heater platform."""
|
||||||
|
data: AOSmithData = hass.data[DOMAIN][entry.entry_id]
|
||||||
|
|
||||||
|
entities = []
|
||||||
|
|
||||||
|
for junction_id in data.coordinator.data:
|
||||||
|
entities.append(AOSmithWaterHeaterEntity(data.coordinator, junction_id))
|
||||||
|
|
||||||
|
async_add_entities(entities)
|
||||||
|
|
||||||
|
|
||||||
|
class AOSmithWaterHeaterEntity(AOSmithEntity, WaterHeaterEntity):
|
||||||
|
"""The water heater entity for the A. O. Smith integration."""
|
||||||
|
|
||||||
|
_attr_name = None
|
||||||
|
_attr_temperature_unit = UnitOfTemperature.FAHRENHEIT
|
||||||
|
_attr_min_temp = 95
|
||||||
|
|
||||||
|
def __init__(self, coordinator: AOSmithCoordinator, junction_id: str) -> None:
|
||||||
|
"""Initialize the entity."""
|
||||||
|
super().__init__(coordinator, junction_id)
|
||||||
|
self._attr_unique_id = junction_id
|
||||||
|
|
||||||
|
@property
|
||||||
|
def operation_list(self) -> list[str]:
|
||||||
|
"""Return the list of supported operation modes."""
|
||||||
|
op_modes = []
|
||||||
|
for mode_dict in self.device_data.get("modes", []):
|
||||||
|
mode_name = mode_dict.get("mode")
|
||||||
|
ha_mode = MODE_AOSMITH_TO_HA.get(mode_name)
|
||||||
|
|
||||||
|
# Filtering out STATE_OFF since it is handled by away mode
|
||||||
|
if ha_mode is not None and ha_mode != STATE_OFF:
|
||||||
|
op_modes.append(ha_mode)
|
||||||
|
|
||||||
|
return op_modes
|
||||||
|
|
||||||
|
@property
|
||||||
|
def supported_features(self) -> WaterHeaterEntityFeature:
|
||||||
|
"""Return the list of supported features."""
|
||||||
|
supports_vacation_mode = any(
|
||||||
|
mode_dict.get("mode") == AOSMITH_MODE_VACATION
|
||||||
|
for mode_dict in self.device_data.get("modes", [])
|
||||||
|
)
|
||||||
|
|
||||||
|
if supports_vacation_mode:
|
||||||
|
return DEFAULT_SUPPORT_FLAGS | WaterHeaterEntityFeature.AWAY_MODE
|
||||||
|
|
||||||
|
return DEFAULT_SUPPORT_FLAGS
|
||||||
|
|
||||||
|
@property
|
||||||
|
def target_temperature(self) -> float | None:
|
||||||
|
"""Return the temperature we try to reach."""
|
||||||
|
return self.device_data.get("temperatureSetpoint")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def max_temp(self) -> float:
|
||||||
|
"""Return the maximum temperature."""
|
||||||
|
return self.device_data.get("temperatureSetpointMaximum")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current_operation(self) -> str:
|
||||||
|
"""Return the current operation mode."""
|
||||||
|
return MODE_AOSMITH_TO_HA.get(self.device_data.get("mode"), STATE_OFF)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_away_mode_on(self):
|
||||||
|
"""Return True if away mode is on."""
|
||||||
|
return self.device_data.get("mode") == AOSMITH_MODE_VACATION
|
||||||
|
|
||||||
|
async def async_set_operation_mode(self, operation_mode: str) -> None:
|
||||||
|
"""Set new target operation mode."""
|
||||||
|
aosmith_mode = MODE_HA_TO_AOSMITH.get(operation_mode)
|
||||||
|
if aosmith_mode is not None:
|
||||||
|
await self.client.update_mode(self.junction_id, aosmith_mode)
|
||||||
|
|
||||||
|
await self.coordinator.async_request_refresh()
|
||||||
|
|
||||||
|
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||||
|
"""Set new target temperature."""
|
||||||
|
temperature = kwargs.get("temperature")
|
||||||
|
await self.client.update_setpoint(self.junction_id, temperature)
|
||||||
|
|
||||||
|
await self.coordinator.async_request_refresh()
|
||||||
|
|
||||||
|
async def async_turn_away_mode_on(self) -> None:
|
||||||
|
"""Turn away mode on."""
|
||||||
|
await self.client.update_mode(self.junction_id, AOSMITH_MODE_VACATION)
|
||||||
|
|
||||||
|
await self.coordinator.async_request_refresh()
|
||||||
|
|
||||||
|
async def async_turn_away_mode_off(self) -> None:
|
||||||
|
"""Turn away mode off."""
|
||||||
|
await self.client.update_mode(self.junction_id, DEFAULT_OPERATION_MODE)
|
||||||
|
|
||||||
|
await self.coordinator.async_request_refresh()
|
@ -46,6 +46,7 @@ FLOWS = {
|
|||||||
"androidtv_remote",
|
"androidtv_remote",
|
||||||
"anova",
|
"anova",
|
||||||
"anthemav",
|
"anthemav",
|
||||||
|
"aosmith",
|
||||||
"apcupsd",
|
"apcupsd",
|
||||||
"apple_tv",
|
"apple_tv",
|
||||||
"aranet",
|
"aranet",
|
||||||
|
@ -286,6 +286,12 @@
|
|||||||
"integration_type": "virtual",
|
"integration_type": "virtual",
|
||||||
"supported_by": "energyzero"
|
"supported_by": "energyzero"
|
||||||
},
|
},
|
||||||
|
"aosmith": {
|
||||||
|
"name": "A. O. Smith",
|
||||||
|
"integration_type": "hub",
|
||||||
|
"config_flow": true,
|
||||||
|
"iot_class": "cloud_polling"
|
||||||
|
},
|
||||||
"apache_kafka": {
|
"apache_kafka": {
|
||||||
"name": "Apache Kafka",
|
"name": "Apache Kafka",
|
||||||
"integration_type": "hub",
|
"integration_type": "hub",
|
||||||
|
@ -1534,6 +1534,9 @@ pushover_complete==1.1.1
|
|||||||
# homeassistant.components.pvoutput
|
# homeassistant.components.pvoutput
|
||||||
pvo==2.1.1
|
pvo==2.1.1
|
||||||
|
|
||||||
|
# homeassistant.components.aosmith
|
||||||
|
py-aosmith==1.0.1
|
||||||
|
|
||||||
# homeassistant.components.canary
|
# homeassistant.components.canary
|
||||||
py-canary==0.5.3
|
py-canary==0.5.3
|
||||||
|
|
||||||
|
@ -1177,6 +1177,9 @@ pushover_complete==1.1.1
|
|||||||
# homeassistant.components.pvoutput
|
# homeassistant.components.pvoutput
|
||||||
pvo==2.1.1
|
pvo==2.1.1
|
||||||
|
|
||||||
|
# homeassistant.components.aosmith
|
||||||
|
py-aosmith==1.0.1
|
||||||
|
|
||||||
# homeassistant.components.canary
|
# homeassistant.components.canary
|
||||||
py-canary==0.5.3
|
py-canary==0.5.3
|
||||||
|
|
||||||
|
1
tests/components/aosmith/__init__.py
Normal file
1
tests/components/aosmith/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
"""Tests for the A. O. Smith integration."""
|
74
tests/components/aosmith/conftest.py
Normal file
74
tests/components/aosmith/conftest.py
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
"""Common fixtures for the A. O. Smith tests."""
|
||||||
|
from collections.abc import Generator
|
||||||
|
from unittest.mock import AsyncMock, MagicMock, patch
|
||||||
|
|
||||||
|
from py_aosmith import AOSmithAPIClient
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant.components.aosmith.const import DOMAIN
|
||||||
|
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry, load_json_array_fixture
|
||||||
|
|
||||||
|
FIXTURE_USER_INPUT = {
|
||||||
|
CONF_EMAIL: "testemail@example.com",
|
||||||
|
CONF_PASSWORD: "test-password",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_config_entry() -> MockConfigEntry:
|
||||||
|
"""Return the default mocked config entry."""
|
||||||
|
return MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
data=FIXTURE_USER_INPUT,
|
||||||
|
unique_id="unique_id",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_setup_entry() -> Generator[AsyncMock, None, None]:
|
||||||
|
"""Override async_setup_entry."""
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.aosmith.async_setup_entry", return_value=True
|
||||||
|
) as mock_setup_entry:
|
||||||
|
yield mock_setup_entry
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def get_devices_fixture() -> str:
|
||||||
|
"""Return the name of the fixture to use for get_devices."""
|
||||||
|
return "get_devices"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
async def mock_client(get_devices_fixture: str) -> Generator[MagicMock, None, None]:
|
||||||
|
"""Return a mocked client."""
|
||||||
|
get_devices_fixture = load_json_array_fixture(f"{get_devices_fixture}.json", DOMAIN)
|
||||||
|
|
||||||
|
client_mock = MagicMock(AOSmithAPIClient)
|
||||||
|
client_mock.get_devices = AsyncMock(return_value=get_devices_fixture)
|
||||||
|
|
||||||
|
return client_mock
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
async def init_integration(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
mock_client: MagicMock,
|
||||||
|
) -> MockConfigEntry:
|
||||||
|
"""Set up the integration for testing."""
|
||||||
|
hass.config.units = US_CUSTOMARY_SYSTEM
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.aosmith.AOSmithAPIClient", return_value=mock_client
|
||||||
|
):
|
||||||
|
mock_config_entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
return mock_config_entry
|
46
tests/components/aosmith/fixtures/get_devices.json
Normal file
46
tests/components/aosmith/fixtures/get_devices.json
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"brand": "aosmith",
|
||||||
|
"model": "HPTS-50 200 202172000",
|
||||||
|
"deviceType": "NEXT_GEN_HEAT_PUMP",
|
||||||
|
"dsn": "dsn",
|
||||||
|
"junctionId": "junctionId",
|
||||||
|
"name": "My water heater",
|
||||||
|
"serial": "serial",
|
||||||
|
"install": {
|
||||||
|
"location": "Basement"
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"__typename": "NextGenHeatPump",
|
||||||
|
"temperatureSetpoint": 130,
|
||||||
|
"temperatureSetpointPending": false,
|
||||||
|
"temperatureSetpointPrevious": 130,
|
||||||
|
"temperatureSetpointMaximum": 130,
|
||||||
|
"modes": [
|
||||||
|
{
|
||||||
|
"mode": "HYBRID",
|
||||||
|
"controls": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"mode": "HEAT_PUMP",
|
||||||
|
"controls": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"mode": "ELECTRIC",
|
||||||
|
"controls": "SELECT_DAYS"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"mode": "VACATION",
|
||||||
|
"controls": "SELECT_DAYS"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isOnline": true,
|
||||||
|
"firmwareVersion": "2.14",
|
||||||
|
"hotWaterStatus": "LOW",
|
||||||
|
"mode": "HEAT_PUMP",
|
||||||
|
"modePending": false,
|
||||||
|
"vacationModeRemainingDays": 0,
|
||||||
|
"electricModeRemainingDays": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
@ -0,0 +1,46 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"brand": "aosmith",
|
||||||
|
"model": "HPTS-50 200 202172000",
|
||||||
|
"deviceType": "NEXT_GEN_HEAT_PUMP",
|
||||||
|
"dsn": "dsn",
|
||||||
|
"junctionId": "junctionId",
|
||||||
|
"name": "My water heater",
|
||||||
|
"serial": "serial",
|
||||||
|
"install": {
|
||||||
|
"location": "Basement"
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"__typename": "NextGenHeatPump",
|
||||||
|
"temperatureSetpoint": 130,
|
||||||
|
"temperatureSetpointPending": false,
|
||||||
|
"temperatureSetpointPrevious": 130,
|
||||||
|
"temperatureSetpointMaximum": 130,
|
||||||
|
"modes": [
|
||||||
|
{
|
||||||
|
"mode": "HYBRID",
|
||||||
|
"controls": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"mode": "HEAT_PUMP",
|
||||||
|
"controls": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"mode": "ELECTRIC",
|
||||||
|
"controls": "SELECT_DAYS"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"mode": "VACATION",
|
||||||
|
"controls": "SELECT_DAYS"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isOnline": true,
|
||||||
|
"firmwareVersion": "2.14",
|
||||||
|
"hotWaterStatus": "LOW",
|
||||||
|
"mode": "HEAT_PUMP",
|
||||||
|
"modePending": true,
|
||||||
|
"vacationModeRemainingDays": 0,
|
||||||
|
"electricModeRemainingDays": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
@ -0,0 +1,42 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"brand": "aosmith",
|
||||||
|
"model": "HPTS-50 200 202172000",
|
||||||
|
"deviceType": "NEXT_GEN_HEAT_PUMP",
|
||||||
|
"dsn": "dsn",
|
||||||
|
"junctionId": "junctionId",
|
||||||
|
"name": "My water heater",
|
||||||
|
"serial": "serial",
|
||||||
|
"install": {
|
||||||
|
"location": "Basement"
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"__typename": "NextGenHeatPump",
|
||||||
|
"temperatureSetpoint": 130,
|
||||||
|
"temperatureSetpointPending": false,
|
||||||
|
"temperatureSetpointPrevious": 130,
|
||||||
|
"temperatureSetpointMaximum": 130,
|
||||||
|
"modes": [
|
||||||
|
{
|
||||||
|
"mode": "HYBRID",
|
||||||
|
"controls": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"mode": "HEAT_PUMP",
|
||||||
|
"controls": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"mode": "ELECTRIC",
|
||||||
|
"controls": "SELECT_DAYS"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isOnline": true,
|
||||||
|
"firmwareVersion": "2.14",
|
||||||
|
"hotWaterStatus": "LOW",
|
||||||
|
"mode": "HEAT_PUMP",
|
||||||
|
"modePending": false,
|
||||||
|
"vacationModeRemainingDays": 0,
|
||||||
|
"electricModeRemainingDays": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
@ -0,0 +1,46 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"brand": "aosmith",
|
||||||
|
"model": "HPTS-50 200 202172000",
|
||||||
|
"deviceType": "NEXT_GEN_HEAT_PUMP",
|
||||||
|
"dsn": "dsn",
|
||||||
|
"junctionId": "junctionId",
|
||||||
|
"name": "My water heater",
|
||||||
|
"serial": "serial",
|
||||||
|
"install": {
|
||||||
|
"location": "Basement"
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"__typename": "NextGenHeatPump",
|
||||||
|
"temperatureSetpoint": 130,
|
||||||
|
"temperatureSetpointPending": true,
|
||||||
|
"temperatureSetpointPrevious": 130,
|
||||||
|
"temperatureSetpointMaximum": 130,
|
||||||
|
"modes": [
|
||||||
|
{
|
||||||
|
"mode": "HYBRID",
|
||||||
|
"controls": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"mode": "HEAT_PUMP",
|
||||||
|
"controls": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"mode": "ELECTRIC",
|
||||||
|
"controls": "SELECT_DAYS"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"mode": "VACATION",
|
||||||
|
"controls": "SELECT_DAYS"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isOnline": true,
|
||||||
|
"firmwareVersion": "2.14",
|
||||||
|
"hotWaterStatus": "LOW",
|
||||||
|
"mode": "HEAT_PUMP",
|
||||||
|
"modePending": false,
|
||||||
|
"vacationModeRemainingDays": 0,
|
||||||
|
"electricModeRemainingDays": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
29
tests/components/aosmith/snapshots/test_device.ambr
Normal file
29
tests/components/aosmith/snapshots/test_device.ambr
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
# serializer version: 1
|
||||||
|
# name: test_device
|
||||||
|
DeviceRegistryEntrySnapshot({
|
||||||
|
'area_id': 'basement',
|
||||||
|
'config_entries': <ANY>,
|
||||||
|
'configuration_url': None,
|
||||||
|
'connections': set({
|
||||||
|
}),
|
||||||
|
'disabled_by': None,
|
||||||
|
'entry_type': None,
|
||||||
|
'hw_version': None,
|
||||||
|
'id': <ANY>,
|
||||||
|
'identifiers': set({
|
||||||
|
tuple(
|
||||||
|
'aosmith',
|
||||||
|
'junctionId',
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
'is_new': False,
|
||||||
|
'manufacturer': 'A. O. Smith',
|
||||||
|
'model': 'HPTS-50 200 202172000',
|
||||||
|
'name': 'My water heater',
|
||||||
|
'name_by_user': None,
|
||||||
|
'serial_number': 'serial',
|
||||||
|
'suggested_area': 'Basement',
|
||||||
|
'sw_version': '2.14',
|
||||||
|
'via_device_id': None,
|
||||||
|
})
|
||||||
|
# ---
|
27
tests/components/aosmith/snapshots/test_water_heater.ambr
Normal file
27
tests/components/aosmith/snapshots/test_water_heater.ambr
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
# serializer version: 1
|
||||||
|
# name: test_state
|
||||||
|
StateSnapshot({
|
||||||
|
'attributes': ReadOnlyDict({
|
||||||
|
'away_mode': 'off',
|
||||||
|
'current_temperature': None,
|
||||||
|
'friendly_name': 'My water heater',
|
||||||
|
'max_temp': 130,
|
||||||
|
'min_temp': 95,
|
||||||
|
'operation_list': list([
|
||||||
|
'eco',
|
||||||
|
'heat_pump',
|
||||||
|
'electric',
|
||||||
|
]),
|
||||||
|
'operation_mode': 'heat_pump',
|
||||||
|
'supported_features': <WaterHeaterEntityFeature: 7>,
|
||||||
|
'target_temp_high': None,
|
||||||
|
'target_temp_low': None,
|
||||||
|
'temperature': 130,
|
||||||
|
}),
|
||||||
|
'context': <ANY>,
|
||||||
|
'entity_id': 'water_heater.my_water_heater',
|
||||||
|
'last_changed': <ANY>,
|
||||||
|
'last_updated': <ANY>,
|
||||||
|
'state': 'heat_pump',
|
||||||
|
})
|
||||||
|
# ---
|
84
tests/components/aosmith/test_config_flow.py
Normal file
84
tests/components/aosmith/test_config_flow.py
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
"""Test the A. O. Smith config flow."""
|
||||||
|
from unittest.mock import AsyncMock, patch
|
||||||
|
|
||||||
|
from py_aosmith import AOSmithInvalidCredentialsException
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant import config_entries
|
||||||
|
from homeassistant.components.aosmith.const import DOMAIN
|
||||||
|
from homeassistant.const import CONF_EMAIL
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.data_entry_flow import FlowResultType
|
||||||
|
|
||||||
|
from tests.components.aosmith.conftest import FIXTURE_USER_INPUT
|
||||||
|
|
||||||
|
|
||||||
|
async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None:
|
||||||
|
"""Test we get the form."""
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||||
|
)
|
||||||
|
assert result["type"] == FlowResultType.FORM
|
||||||
|
assert result["errors"] == {}
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.aosmith.config_flow.AOSmithAPIClient.get_devices",
|
||||||
|
return_value=[],
|
||||||
|
):
|
||||||
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
FIXTURE_USER_INPUT,
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert result2["type"] == FlowResultType.CREATE_ENTRY
|
||||||
|
assert result2["title"] == FIXTURE_USER_INPUT[CONF_EMAIL]
|
||||||
|
assert result2["data"] == FIXTURE_USER_INPUT
|
||||||
|
assert len(mock_setup_entry.mock_calls) == 1
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("exception", "expected_error_key"),
|
||||||
|
[
|
||||||
|
(AOSmithInvalidCredentialsException("Invalid credentials"), "invalid_auth"),
|
||||||
|
(Exception, "unknown"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_form_exception(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_setup_entry: AsyncMock,
|
||||||
|
exception: Exception,
|
||||||
|
expected_error_key: str,
|
||||||
|
) -> None:
|
||||||
|
"""Test handling an exception and then recovering on the second attempt."""
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||||
|
)
|
||||||
|
assert result["type"] == FlowResultType.FORM
|
||||||
|
assert result["errors"] == {}
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.aosmith.config_flow.AOSmithAPIClient.get_devices",
|
||||||
|
side_effect=exception,
|
||||||
|
):
|
||||||
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
FIXTURE_USER_INPUT,
|
||||||
|
)
|
||||||
|
assert result2["type"] == FlowResultType.FORM
|
||||||
|
assert result2["errors"] == {"base": expected_error_key}
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.aosmith.config_flow.AOSmithAPIClient.get_devices",
|
||||||
|
return_value=[],
|
||||||
|
):
|
||||||
|
result3 = await hass.config_entries.flow.async_configure(
|
||||||
|
result2["flow_id"],
|
||||||
|
FIXTURE_USER_INPUT,
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert result3["type"] == FlowResultType.CREATE_ENTRY
|
||||||
|
assert result3["title"] == FIXTURE_USER_INPUT[CONF_EMAIL]
|
||||||
|
assert result3["data"] == FIXTURE_USER_INPUT
|
||||||
|
assert len(mock_setup_entry.mock_calls) == 1
|
23
tests/components/aosmith/test_device.py
Normal file
23
tests/components/aosmith/test_device.py
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
"""Tests for the device created by the A. O. Smith integration."""
|
||||||
|
|
||||||
|
from syrupy.assertion import SnapshotAssertion
|
||||||
|
|
||||||
|
from homeassistant.components.aosmith.const import DOMAIN
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers import device_registry as dr
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
|
||||||
|
async def test_device(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
device_registry: dr.DeviceRegistry,
|
||||||
|
init_integration: MockConfigEntry,
|
||||||
|
snapshot: SnapshotAssertion,
|
||||||
|
) -> None:
|
||||||
|
"""Test creation of the device."""
|
||||||
|
reg_device = device_registry.async_get_device(
|
||||||
|
identifiers={(DOMAIN, "junctionId")},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert reg_device == snapshot
|
71
tests/components/aosmith/test_init.py
Normal file
71
tests/components/aosmith/test_init.py
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
"""Tests for the initialization of the A. O. Smith integration."""
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
from freezegun.api import FrozenDateTimeFactory
|
||||||
|
from py_aosmith import AOSmithUnknownException
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant.components.aosmith.const import (
|
||||||
|
DOMAIN,
|
||||||
|
FAST_INTERVAL,
|
||||||
|
REGULAR_INTERVAL,
|
||||||
|
)
|
||||||
|
from homeassistant.config_entries import ConfigEntryState
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||||
|
|
||||||
|
|
||||||
|
async def test_config_entry_setup(init_integration: MockConfigEntry) -> None:
|
||||||
|
"""Test setup of the config entry."""
|
||||||
|
mock_config_entry = init_integration
|
||||||
|
|
||||||
|
assert mock_config_entry.state is ConfigEntryState.LOADED
|
||||||
|
|
||||||
|
|
||||||
|
async def test_config_entry_not_ready(
|
||||||
|
hass: HomeAssistant, mock_config_entry: MockConfigEntry
|
||||||
|
) -> None:
|
||||||
|
"""Test the config entry not ready."""
|
||||||
|
mock_config_entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.aosmith.config_flow.AOSmithAPIClient.get_devices",
|
||||||
|
side_effect=AOSmithUnknownException("Unknown error"),
|
||||||
|
):
|
||||||
|
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("get_devices_fixture", "time_to_wait", "expected_call_count"),
|
||||||
|
[
|
||||||
|
("get_devices", REGULAR_INTERVAL, 1),
|
||||||
|
("get_devices", FAST_INTERVAL, 0),
|
||||||
|
("get_devices_mode_pending", FAST_INTERVAL, 1),
|
||||||
|
("get_devices_setpoint_pending", FAST_INTERVAL, 1),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_update(
|
||||||
|
freezer: FrozenDateTimeFactory,
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_client: MagicMock,
|
||||||
|
init_integration: MockConfigEntry,
|
||||||
|
time_to_wait: timedelta,
|
||||||
|
expected_call_count: int,
|
||||||
|
) -> None:
|
||||||
|
"""Test data update with differing intervals depending on device status."""
|
||||||
|
entries = hass.config_entries.async_entries(DOMAIN)
|
||||||
|
assert len(entries) == 1
|
||||||
|
assert entries[0].state is ConfigEntryState.LOADED
|
||||||
|
assert mock_client.get_devices.call_count == 1
|
||||||
|
|
||||||
|
freezer.tick(time_to_wait)
|
||||||
|
async_fire_time_changed(hass)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert mock_client.get_devices.call_count == 1 + expected_call_count
|
147
tests/components/aosmith/test_water_heater.py
Normal file
147
tests/components/aosmith/test_water_heater.py
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
"""Tests for the water heater platform of the A. O. Smith integration."""
|
||||||
|
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from syrupy.assertion import SnapshotAssertion
|
||||||
|
|
||||||
|
from homeassistant.components.aosmith.const import (
|
||||||
|
AOSMITH_MODE_ELECTRIC,
|
||||||
|
AOSMITH_MODE_HEAT_PUMP,
|
||||||
|
AOSMITH_MODE_HYBRID,
|
||||||
|
AOSMITH_MODE_VACATION,
|
||||||
|
)
|
||||||
|
from homeassistant.components.water_heater import (
|
||||||
|
ATTR_AWAY_MODE,
|
||||||
|
ATTR_OPERATION_MODE,
|
||||||
|
ATTR_TEMPERATURE,
|
||||||
|
DOMAIN as WATER_HEATER_DOMAIN,
|
||||||
|
SERVICE_SET_AWAY_MODE,
|
||||||
|
SERVICE_SET_OPERATION_MODE,
|
||||||
|
SERVICE_SET_TEMPERATURE,
|
||||||
|
STATE_ECO,
|
||||||
|
STATE_ELECTRIC,
|
||||||
|
STATE_HEAT_PUMP,
|
||||||
|
WaterHeaterEntityFeature,
|
||||||
|
)
|
||||||
|
from homeassistant.const import (
|
||||||
|
ATTR_ENTITY_ID,
|
||||||
|
ATTR_FRIENDLY_NAME,
|
||||||
|
ATTR_SUPPORTED_FEATURES,
|
||||||
|
)
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers import entity_registry as er
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
|
||||||
|
async def test_setup(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entity_registry: er.EntityRegistry,
|
||||||
|
init_integration: MockConfigEntry,
|
||||||
|
) -> None:
|
||||||
|
"""Test the setup of the water heater entity."""
|
||||||
|
entry = entity_registry.async_get("water_heater.my_water_heater")
|
||||||
|
assert entry
|
||||||
|
assert entry.unique_id == "junctionId"
|
||||||
|
|
||||||
|
state = hass.states.get("water_heater.my_water_heater")
|
||||||
|
assert state
|
||||||
|
assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My water heater"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_state(
|
||||||
|
hass: HomeAssistant, init_integration: MockConfigEntry, snapshot: SnapshotAssertion
|
||||||
|
) -> None:
|
||||||
|
"""Test the state of the water heater entity."""
|
||||||
|
state = hass.states.get("water_heater.my_water_heater")
|
||||||
|
assert state == snapshot
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("get_devices_fixture"),
|
||||||
|
["get_devices_no_vacation_mode"],
|
||||||
|
)
|
||||||
|
async def test_state_away_mode_unsupported(
|
||||||
|
hass: HomeAssistant, init_integration: MockConfigEntry
|
||||||
|
) -> None:
|
||||||
|
"""Test that away mode is not supported if the water heater does not support vacation mode."""
|
||||||
|
state = hass.states.get("water_heater.my_water_heater")
|
||||||
|
assert (
|
||||||
|
state.attributes.get(ATTR_SUPPORTED_FEATURES)
|
||||||
|
== WaterHeaterEntityFeature.TARGET_TEMPERATURE
|
||||||
|
| WaterHeaterEntityFeature.OPERATION_MODE
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("hass_mode", "aosmith_mode"),
|
||||||
|
[
|
||||||
|
(STATE_HEAT_PUMP, AOSMITH_MODE_HEAT_PUMP),
|
||||||
|
(STATE_ECO, AOSMITH_MODE_HYBRID),
|
||||||
|
(STATE_ELECTRIC, AOSMITH_MODE_ELECTRIC),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_set_operation_mode(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_client: MagicMock,
|
||||||
|
init_integration: MockConfigEntry,
|
||||||
|
hass_mode: str,
|
||||||
|
aosmith_mode: str,
|
||||||
|
) -> None:
|
||||||
|
"""Test setting the operation mode."""
|
||||||
|
await hass.services.async_call(
|
||||||
|
WATER_HEATER_DOMAIN,
|
||||||
|
SERVICE_SET_OPERATION_MODE,
|
||||||
|
{
|
||||||
|
ATTR_ENTITY_ID: "water_heater.my_water_heater",
|
||||||
|
ATTR_OPERATION_MODE: hass_mode,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
mock_client.update_mode.assert_called_once_with("junctionId", aosmith_mode)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_set_temperature(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_client: MagicMock,
|
||||||
|
init_integration: MockConfigEntry,
|
||||||
|
) -> None:
|
||||||
|
"""Test setting the target temperature."""
|
||||||
|
await hass.services.async_call(
|
||||||
|
WATER_HEATER_DOMAIN,
|
||||||
|
SERVICE_SET_TEMPERATURE,
|
||||||
|
{ATTR_ENTITY_ID: "water_heater.my_water_heater", ATTR_TEMPERATURE: 120},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
mock_client.update_setpoint.assert_called_once_with("junctionId", 120)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("hass_away_mode", "aosmith_mode"),
|
||||||
|
[
|
||||||
|
(True, AOSMITH_MODE_VACATION),
|
||||||
|
(False, AOSMITH_MODE_HYBRID),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_away_mode(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_client: MagicMock,
|
||||||
|
init_integration: MockConfigEntry,
|
||||||
|
hass_away_mode: bool,
|
||||||
|
aosmith_mode: str,
|
||||||
|
) -> None:
|
||||||
|
"""Test turning away mode on/off."""
|
||||||
|
await hass.services.async_call(
|
||||||
|
WATER_HEATER_DOMAIN,
|
||||||
|
SERVICE_SET_AWAY_MODE,
|
||||||
|
{
|
||||||
|
ATTR_ENTITY_ID: "water_heater.my_water_heater",
|
||||||
|
ATTR_AWAY_MODE: hass_away_mode,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
mock_client.update_mode.assert_called_once_with("junctionId", aosmith_mode)
|
Loading…
x
Reference in New Issue
Block a user