This commit is contained in:
Franck Nijhof 2024-10-18 17:06:51 +02:00 committed by GitHub
commit a301d51fb2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
38 changed files with 446 additions and 71 deletions

View file

@ -85,6 +85,7 @@ HVAC_MODE_LIB_TO_HASS: Final[dict[OperationMode, HVACMode]] = {
OperationMode.HEATING: HVACMode.HEAT, OperationMode.HEATING: HVACMode.HEAT,
OperationMode.FAN: HVACMode.FAN_ONLY, OperationMode.FAN: HVACMode.FAN_ONLY,
OperationMode.DRY: HVACMode.DRY, OperationMode.DRY: HVACMode.DRY,
OperationMode.AUX_HEATING: HVACMode.HEAT,
OperationMode.AUTO: HVACMode.HEAT_COOL, OperationMode.AUTO: HVACMode.HEAT_COOL,
} }
HVAC_MODE_HASS_TO_LIB: Final[dict[HVACMode, OperationMode]] = { HVAC_MODE_HASS_TO_LIB: Final[dict[HVACMode, OperationMode]] = {
@ -157,9 +158,10 @@ class AirzoneClimate(AirzoneZoneEntity, ClimateEntity):
self._attr_temperature_unit = TEMP_UNIT_LIB_TO_HASS[ self._attr_temperature_unit = TEMP_UNIT_LIB_TO_HASS[
self.get_airzone_value(AZD_TEMP_UNIT) self.get_airzone_value(AZD_TEMP_UNIT)
] ]
self._attr_hvac_modes = [ _attr_hvac_modes = [
HVAC_MODE_LIB_TO_HASS[mode] for mode in self.get_airzone_value(AZD_MODES) HVAC_MODE_LIB_TO_HASS[mode] for mode in self.get_airzone_value(AZD_MODES)
] ]
self._attr_hvac_modes = list(dict.fromkeys(_attr_hvac_modes))
if ( if (
self.get_airzone_value(AZD_SPEED) is not None self.get_airzone_value(AZD_SPEED) is not None
and self.get_airzone_value(AZD_SPEEDS) is not None and self.get_airzone_value(AZD_SPEEDS) is not None

View file

@ -11,5 +11,5 @@
"documentation": "https://www.home-assistant.io/integrations/airzone", "documentation": "https://www.home-assistant.io/integrations/airzone",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["aioairzone"], "loggers": ["aioairzone"],
"requirements": ["aioairzone==0.9.3"] "requirements": ["aioairzone==0.9.5"]
} }

View file

@ -6,7 +6,7 @@
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/bluesound", "documentation": "https://www.home-assistant.io/integrations/bluesound",
"iot_class": "local_polling", "iot_class": "local_polling",
"requirements": ["pyblu==1.0.3"], "requirements": ["pyblu==1.0.4"],
"zeroconf": [ "zeroconf": [
{ {
"type": "_musc._tcp.local." "type": "_musc._tcp.local."

View file

@ -493,6 +493,8 @@ class BluesoundPlayer(MediaPlayerEntity):
return None return None
position = self._status.seconds position = self._status.seconds
if position is None:
return None
if mediastate == MediaPlayerState.PLAYING: if mediastate == MediaPlayerState.PLAYING:
position += (dt_util.utcnow() - self._last_status_update).total_seconds() position += (dt_util.utcnow() - self._last_status_update).total_seconds()

View file

@ -159,6 +159,7 @@ class DaikinClimate(DaikinEntity, ClimateEntity):
if values: if values:
await self.device.set(values) await self.device.set(values)
await self.coordinator.async_refresh()
@property @property
def unique_id(self) -> str: def unique_id(self) -> str:
@ -261,6 +262,7 @@ class DaikinClimate(DaikinEntity, ClimateEntity):
await self.device.set_advanced_mode( await self.device.set_advanced_mode(
HA_PRESET_TO_DAIKIN[PRESET_ECO], ATTR_STATE_OFF HA_PRESET_TO_DAIKIN[PRESET_ECO], ATTR_STATE_OFF
) )
await self.coordinator.async_refresh()
@property @property
def preset_modes(self) -> list[str]: def preset_modes(self) -> list[str]:
@ -275,9 +277,11 @@ class DaikinClimate(DaikinEntity, ClimateEntity):
async def async_turn_on(self) -> None: async def async_turn_on(self) -> None:
"""Turn device on.""" """Turn device on."""
await self.device.set({}) await self.device.set({})
await self.coordinator.async_refresh()
async def async_turn_off(self) -> None: async def async_turn_off(self) -> None:
"""Turn device off.""" """Turn device off."""
await self.device.set( await self.device.set(
{HA_ATTR_TO_DAIKIN[ATTR_HVAC_MODE]: HA_STATE_TO_DAIKIN[HVACMode.OFF]} {HA_ATTR_TO_DAIKIN[ATTR_HVAC_MODE]: HA_STATE_TO_DAIKIN[HVACMode.OFF]}
) )
await self.coordinator.async_refresh()

View file

@ -63,10 +63,12 @@ class DaikinZoneSwitch(DaikinEntity, SwitchEntity):
async def async_turn_on(self, **kwargs: Any) -> None: async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the zone on.""" """Turn the zone on."""
await self.device.set_zone(self._zone_id, "zone_onoff", "1") await self.device.set_zone(self._zone_id, "zone_onoff", "1")
await self.coordinator.async_refresh()
async def async_turn_off(self, **kwargs: Any) -> None: async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the zone off.""" """Turn the zone off."""
await self.device.set_zone(self._zone_id, "zone_onoff", "0") await self.device.set_zone(self._zone_id, "zone_onoff", "0")
await self.coordinator.async_refresh()
class DaikinStreamerSwitch(DaikinEntity, SwitchEntity): class DaikinStreamerSwitch(DaikinEntity, SwitchEntity):
@ -88,10 +90,12 @@ class DaikinStreamerSwitch(DaikinEntity, SwitchEntity):
async def async_turn_on(self, **kwargs: Any) -> None: async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the zone on.""" """Turn the zone on."""
await self.device.set_streamer("on") await self.device.set_streamer("on")
await self.coordinator.async_refresh()
async def async_turn_off(self, **kwargs: Any) -> None: async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the zone off.""" """Turn the zone off."""
await self.device.set_streamer("off") await self.device.set_streamer("off")
await self.coordinator.async_refresh()
class DaikinToggleSwitch(DaikinEntity, SwitchEntity): class DaikinToggleSwitch(DaikinEntity, SwitchEntity):
@ -112,7 +116,9 @@ class DaikinToggleSwitch(DaikinEntity, SwitchEntity):
async def async_turn_on(self, **kwargs: Any) -> None: async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the zone on.""" """Turn the zone on."""
await self.device.set({}) await self.device.set({})
await self.coordinator.async_refresh()
async def async_turn_off(self, **kwargs: Any) -> None: async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the zone off.""" """Turn the zone off."""
await self.device.set({DAIKIN_ATTR_MODE: "off"}) await self.device.set({DAIKIN_ATTR_MODE: "off"})
await self.coordinator.async_refresh()

View file

@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/calendar.google", "documentation": "https://www.home-assistant.io/integrations/calendar.google",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["googleapiclient"], "loggers": ["googleapiclient"],
"requirements": ["gcal-sync==6.1.5", "oauth2client==4.1.3", "ical==8.2.0"] "requirements": ["gcal-sync==6.1.6", "oauth2client==4.1.3", "ical==8.2.0"]
} }

View file

@ -8,6 +8,6 @@
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["deepmerge", "pyipp"], "loggers": ["deepmerge", "pyipp"],
"quality_scale": "platinum", "quality_scale": "platinum",
"requirements": ["pyipp==0.16.0"], "requirements": ["pyipp==0.17.0"],
"zeroconf": ["_ipps._tcp.local.", "_ipp._tcp.local."] "zeroconf": ["_ipps._tcp.local.", "_ipp._tcp.local."]
} }

View file

@ -4,7 +4,7 @@ from __future__ import annotations
from collections.abc import Callable from collections.abc import Callable
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime, timedelta from datetime import datetime
from typing import Any from typing import Any
from pyipp import Marker, Printer from pyipp import Marker, Printer
@ -19,7 +19,6 @@ from homeassistant.const import ATTR_LOCATION, PERCENTAGE, EntityCategory
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType from homeassistant.helpers.typing import StateType
from homeassistant.util.dt import utcnow
from . import IPPConfigEntry from . import IPPConfigEntry
from .const import ( from .const import (
@ -80,7 +79,7 @@ PRINTER_SENSORS: tuple[IPPSensorEntityDescription, ...] = (
device_class=SensorDeviceClass.TIMESTAMP, device_class=SensorDeviceClass.TIMESTAMP,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
value_fn=lambda printer: (utcnow() - timedelta(seconds=printer.info.uptime)), value_fn=lambda printer: printer.booted_at,
), ),
) )

View file

@ -8,6 +8,6 @@
"iot_class": "calculated", "iot_class": "calculated",
"loggers": ["yt_dlp"], "loggers": ["yt_dlp"],
"quality_scale": "internal", "quality_scale": "internal",
"requirements": ["yt-dlp==2024.09.27"], "requirements": ["yt-dlp==2024.10.07"],
"single_config_entry": true "single_config_entry": true
} }

View file

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/opentherm_gw", "documentation": "https://www.home-assistant.io/integrations/opentherm_gw",
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["pyotgw"], "loggers": ["pyotgw"],
"requirements": ["pyotgw==2.2.1"] "requirements": ["pyotgw==2.2.2"]
} }

View file

@ -6,7 +6,7 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, Platform from homeassistant.const import CONF_HOST, Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from .const import DOMAIN from .const import CONF_PLAY_MEDIA_APP_ID, DEFAULT_PLAY_MEDIA_APP_ID, DOMAIN
from .coordinator import RokuDataUpdateCoordinator from .coordinator import RokuDataUpdateCoordinator
PLATFORMS = [ PLATFORMS = [
@ -24,7 +24,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
device_id = entry.entry_id device_id = entry.entry_id
coordinator = RokuDataUpdateCoordinator( coordinator = RokuDataUpdateCoordinator(
hass, host=entry.data[CONF_HOST], device_id=device_id hass,
host=entry.data[CONF_HOST],
device_id=device_id,
play_media_app_id=entry.options.get(
CONF_PLAY_MEDIA_APP_ID, DEFAULT_PLAY_MEDIA_APP_ID
),
) )
await coordinator.async_config_entry_first_refresh() await coordinator.async_config_entry_first_refresh()
@ -32,6 +37,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
entry.async_on_unload(entry.add_update_listener(async_reload_entry))
return True return True
@ -40,3 +47,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
hass.data[DOMAIN].pop(entry.entry_id) hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok return unload_ok
async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Reload the config entry when it changed."""
await hass.config_entries.async_reload(entry.entry_id)

View file

@ -10,12 +10,17 @@ from rokuecp import Roku, RokuError
import voluptuous as vol import voluptuous as vol
from homeassistant.components import ssdp, zeroconf from homeassistant.components import ssdp, zeroconf
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.config_entries import (
ConfigEntry,
ConfigFlow,
ConfigFlowResult,
OptionsFlowWithConfigEntry,
)
from homeassistant.const import CONF_HOST, CONF_NAME from homeassistant.const import CONF_HOST, CONF_NAME
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DOMAIN from .const import CONF_PLAY_MEDIA_APP_ID, DEFAULT_PLAY_MEDIA_APP_ID, DOMAIN
DATA_SCHEMA = vol.Schema({vol.Required(CONF_HOST): str}) DATA_SCHEMA = vol.Schema({vol.Required(CONF_HOST): str})
@ -155,3 +160,36 @@ class RokuConfigFlow(ConfigFlow, domain=DOMAIN):
title=self.discovery_info[CONF_NAME], title=self.discovery_info[CONF_NAME],
data=self.discovery_info, data=self.discovery_info,
) )
@staticmethod
@callback
def async_get_options_flow(
config_entry: ConfigEntry,
) -> OptionsFlowWithConfigEntry:
"""Create the options flow."""
return RokuOptionsFlowHandler(config_entry)
class RokuOptionsFlowHandler(OptionsFlowWithConfigEntry):
"""Handle Roku options."""
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Manage Roku options."""
if user_input is not None:
return self.async_create_entry(title="", data=user_input)
return self.async_show_form(
step_id="init",
data_schema=vol.Schema(
{
vol.Optional(
CONF_PLAY_MEDIA_APP_ID,
default=self.options.get(
CONF_PLAY_MEDIA_APP_ID, DEFAULT_PLAY_MEDIA_APP_ID
),
): str,
}
),
)

View file

@ -15,3 +15,9 @@ DEFAULT_PORT = 8060
# Services # Services
SERVICE_SEARCH = "search" SERVICE_SEARCH = "search"
# Config
CONF_PLAY_MEDIA_APP_ID = "play_media_app_id"
# Defaults
DEFAULT_PLAY_MEDIA_APP_ID = "15985"

View file

@ -29,15 +29,12 @@ class RokuDataUpdateCoordinator(DataUpdateCoordinator[Device]):
roku: Roku roku: Roku
def __init__( def __init__(
self, self, hass: HomeAssistant, *, host: str, device_id: str, play_media_app_id: str
hass: HomeAssistant,
*,
host: str,
device_id: str,
) -> None: ) -> None:
"""Initialize global Roku data updater.""" """Initialize global Roku data updater."""
self.device_id = device_id self.device_id = device_id
self.roku = Roku(host=host, session=async_get_clientsession(hass)) self.roku = Roku(host=host, session=async_get_clientsession(hass))
self.play_media_app_id = play_media_app_id
self.full_update_interval = timedelta(minutes=15) self.full_update_interval = timedelta(minutes=15)
self.last_full_update = None self.last_full_update = None

View file

@ -445,17 +445,25 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity):
if attr in extra if attr in extra
} }
params = {"t": "a", **params} params = {"u": media_id, "t": "a", **params}
await self.coordinator.roku.play_on_roku(media_id, params) await self.coordinator.roku.launch(
self.coordinator.play_media_app_id,
params,
)
elif media_type in {MediaType.URL, MediaType.VIDEO}: elif media_type in {MediaType.URL, MediaType.VIDEO}:
params = { params = {
param: extra[attr] param: extra[attr]
for (attr, param) in ATTRS_TO_PLAY_ON_ROKU_PARAMS.items() for (attr, param) in ATTRS_TO_PLAY_ON_ROKU_PARAMS.items()
if attr in extra if attr in extra
} }
params["u"] = media_id
params["t"] = "v"
await self.coordinator.roku.play_on_roku(media_id, params) await self.coordinator.roku.launch(
self.coordinator.play_media_app_id,
params,
)
else: else:
_LOGGER.error("Media type %s is not supported", original_media_type) _LOGGER.error("Media type %s is not supported", original_media_type)
return return

View file

@ -24,6 +24,18 @@
"unknown": "[%key:common::config_flow::error::unknown%]" "unknown": "[%key:common::config_flow::error::unknown%]"
} }
}, },
"options": {
"step": {
"init": {
"data": {
"play_media_app_id": "Play Media Roku Application ID"
},
"data_description": {
"play_media_app_id": "The application ID to use when launching media playback. Must support the PlayOnRoku API."
}
}
}
},
"entity": { "entity": {
"binary_sensor": { "binary_sensor": {
"headphones_connected": { "headphones_connected": {

View file

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/solarlog", "documentation": "https://www.home-assistant.io/integrations/solarlog",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["solarlog_cli"], "loggers": ["solarlog_cli"],
"requirements": ["solarlog_cli==0.3.1"] "requirements": ["solarlog_cli==0.3.2"]
} }

View file

@ -84,7 +84,9 @@ def _create_entry(
original_name=f"{DEFAULT_NAME} {tag_id}", original_name=f"{DEFAULT_NAME} {tag_id}",
suggested_object_id=slugify(name) if name else tag_id, suggested_object_id=slugify(name) if name else tag_id,
) )
return entity_registry.async_update_entity(entry.entity_id, name=name) if name:
return entity_registry.async_update_entity(entry.entity_id, name=name)
return entry
class TagStore(Store[collection.SerializedStorageCollection]): class TagStore(Store[collection.SerializedStorageCollection]):

View file

@ -2,6 +2,7 @@
from __future__ import annotations from __future__ import annotations
import ipaddress
import logging import logging
from typing import Any from typing import Any
@ -38,7 +39,19 @@ class WebControlProConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle the DHCP discovery step.""" """Handle the DHCP discovery step."""
unique_id = format_mac(discovery_info.macaddress) unique_id = format_mac(discovery_info.macaddress)
await self.async_set_unique_id(unique_id) await self.async_set_unique_id(unique_id)
self._abort_if_unique_id_configured()
entry = self.hass.config_entries.async_entry_for_domain_unique_id(
DOMAIN, unique_id
)
if entry:
try: # Check if current host is a valid IP address
ipaddress.ip_address(entry.data[CONF_HOST])
except ValueError: # Do not touch name-based host
return self.async_abort(reason="already_configured")
else: # Update existing host with new IP address
self._abort_if_unique_id_configured(
updates={CONF_HOST: discovery_info.ip}
)
for entry in self.hass.config_entries.async_entries(DOMAIN): for entry in self.hass.config_entries.async_entries(DOMAIN):
if not entry.unique_id and entry.data[CONF_HOST] in ( if not entry.unique_id and entry.data[CONF_HOST] in (

View file

@ -1198,7 +1198,7 @@ def create_zha_config(hass: HomeAssistant, ha_zha_data: HAZHAData) -> ZHAData:
# deep copy the yaml config to avoid modifying the original and to safely # deep copy the yaml config to avoid modifying the original and to safely
# pass it to the ZHA library # pass it to the ZHA library
app_config = copy.deepcopy(ha_zha_data.yaml_config.get(CONF_ZIGPY, {})) app_config = copy.deepcopy(ha_zha_data.yaml_config.get(CONF_ZIGPY, {}))
database = app_config.get( database = ha_zha_data.yaml_config.get(
CONF_DATABASE, CONF_DATABASE,
hass.config.path(DEFAULT_DATABASE_NAME), hass.config.path(DEFAULT_DATABASE_NAME),
) )

View file

@ -24,8 +24,6 @@ from homeassistant.components.climate import (
ATTR_HVAC_MODE, ATTR_HVAC_MODE,
ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_HIGH,
ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_LOW,
DEFAULT_MAX_TEMP,
DEFAULT_MIN_TEMP,
DOMAIN as CLIMATE_DOMAIN, DOMAIN as CLIMATE_DOMAIN,
PRESET_NONE, PRESET_NONE,
ClimateEntity, ClimateEntity,
@ -421,7 +419,7 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity):
@property @property
def min_temp(self) -> float: def min_temp(self) -> float:
"""Return the minimum temperature.""" """Return the minimum temperature."""
min_temp = DEFAULT_MIN_TEMP min_temp = 0.0 # Not using DEFAULT_MIN_TEMP to allow wider range
base_unit: str = UnitOfTemperature.CELSIUS base_unit: str = UnitOfTemperature.CELSIUS
try: try:
temp = self._setpoint_value_or_raise(self._current_mode_setpoint_enums[0]) temp = self._setpoint_value_or_raise(self._current_mode_setpoint_enums[0])
@ -437,7 +435,7 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity):
@property @property
def max_temp(self) -> float: def max_temp(self) -> float:
"""Return the maximum temperature.""" """Return the maximum temperature."""
max_temp = DEFAULT_MAX_TEMP max_temp = 50.0 # Not using DEFAULT_MAX_TEMP to allow wider range
base_unit: str = UnitOfTemperature.CELSIUS base_unit: str = UnitOfTemperature.CELSIUS
try: try:
temp = self._setpoint_value_or_raise(self._current_mode_setpoint_enums[0]) temp = self._setpoint_value_or_raise(self._current_mode_setpoint_enums[0])

View file

@ -24,7 +24,7 @@ if TYPE_CHECKING:
APPLICATION_NAME: Final = "HomeAssistant" APPLICATION_NAME: Final = "HomeAssistant"
MAJOR_VERSION: Final = 2024 MAJOR_VERSION: Final = 2024
MINOR_VERSION: Final = 10 MINOR_VERSION: Final = 10
PATCH_VERSION: Final = "2" PATCH_VERSION: Final = "3"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0)

View file

@ -31,7 +31,7 @@ ha-ffmpeg==3.2.0
habluetooth==3.4.0 habluetooth==3.4.0
hass-nabucasa==0.81.1 hass-nabucasa==0.81.1
hassil==1.7.4 hassil==1.7.4
home-assistant-bluetooth==1.12.2 home-assistant-bluetooth==1.13.0
home-assistant-frontend==20241002.3 home-assistant-frontend==20241002.3
home-assistant-intents==2024.10.2 home-assistant-intents==2024.10.2
httpx==0.27.2 httpx==0.27.2

View file

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "homeassistant" name = "homeassistant"
version = "2024.10.2" version = "2024.10.3"
license = {text = "Apache-2.0"} license = {text = "Apache-2.0"}
description = "Open-source home automation platform running on Python 3." description = "Open-source home automation platform running on Python 3."
readme = "README.rst" readme = "README.rst"
@ -46,7 +46,7 @@ dependencies = [
# When bumping httpx, please check the version pins of # When bumping httpx, please check the version pins of
# httpcore, anyio, and h11 in gen_requirements_all # httpcore, anyio, and h11 in gen_requirements_all
"httpx==0.27.2", "httpx==0.27.2",
"home-assistant-bluetooth==1.12.2", "home-assistant-bluetooth==1.13.0",
"ifaddr==0.2.0", "ifaddr==0.2.0",
"Jinja2==3.1.4", "Jinja2==3.1.4",
"lru-dict==1.3.0", "lru-dict==1.3.0",

View file

@ -20,7 +20,7 @@ ciso8601==2.3.1
fnv-hash-fast==1.0.2 fnv-hash-fast==1.0.2
hass-nabucasa==0.81.1 hass-nabucasa==0.81.1
httpx==0.27.2 httpx==0.27.2
home-assistant-bluetooth==1.12.2 home-assistant-bluetooth==1.13.0
ifaddr==0.2.0 ifaddr==0.2.0
Jinja2==3.1.4 Jinja2==3.1.4
lru-dict==1.3.0 lru-dict==1.3.0

View file

@ -179,7 +179,7 @@ aioairq==0.3.2
aioairzone-cloud==0.6.6 aioairzone-cloud==0.6.6
# homeassistant.components.airzone # homeassistant.components.airzone
aioairzone==0.9.3 aioairzone==0.9.5
# homeassistant.components.ambient_network # homeassistant.components.ambient_network
# homeassistant.components.ambient_station # homeassistant.components.ambient_station
@ -945,7 +945,7 @@ gardena-bluetooth==1.4.3
gassist-text==0.0.11 gassist-text==0.0.11
# homeassistant.components.google # homeassistant.components.google
gcal-sync==6.1.5 gcal-sync==6.1.6
# homeassistant.components.geniushub # homeassistant.components.geniushub
geniushub-client==0.7.1 geniushub-client==0.7.1
@ -1780,7 +1780,7 @@ pybbox==0.0.5-alpha
pyblackbird==0.6 pyblackbird==0.6
# homeassistant.components.bluesound # homeassistant.components.bluesound
pyblu==1.0.3 pyblu==1.0.4
# homeassistant.components.neato # homeassistant.components.neato
pybotvac==0.0.25 pybotvac==0.0.25
@ -1960,7 +1960,7 @@ pyintesishome==1.8.0
pyipma==3.0.7 pyipma==3.0.7
# homeassistant.components.ipp # homeassistant.components.ipp
pyipp==0.16.0 pyipp==0.17.0
# homeassistant.components.iqvia # homeassistant.components.iqvia
pyiqvia==2022.04.0 pyiqvia==2022.04.0
@ -2119,7 +2119,7 @@ pyoppleio-legacy==1.0.8
pyosoenergyapi==1.1.4 pyosoenergyapi==1.1.4
# homeassistant.components.opentherm_gw # homeassistant.components.opentherm_gw
pyotgw==2.2.1 pyotgw==2.2.2
# homeassistant.auth.mfa_modules.notify # homeassistant.auth.mfa_modules.notify
# homeassistant.auth.mfa_modules.totp # homeassistant.auth.mfa_modules.totp
@ -2676,7 +2676,7 @@ soco==0.30.4
solaredge-local==0.2.3 solaredge-local==0.2.3
# homeassistant.components.solarlog # homeassistant.components.solarlog
solarlog_cli==0.3.1 solarlog_cli==0.3.2
# homeassistant.components.solax # homeassistant.components.solax
solax==3.1.1 solax==3.1.1
@ -3032,7 +3032,7 @@ youless-api==2.1.2
youtubeaio==1.1.5 youtubeaio==1.1.5
# homeassistant.components.media_extractor # homeassistant.components.media_extractor
yt-dlp==2024.09.27 yt-dlp==2024.10.07
# homeassistant.components.zamg # homeassistant.components.zamg
zamg==0.3.6 zamg==0.3.6

View file

@ -167,7 +167,7 @@ aioairq==0.3.2
aioairzone-cloud==0.6.6 aioairzone-cloud==0.6.6
# homeassistant.components.airzone # homeassistant.components.airzone
aioairzone==0.9.3 aioairzone==0.9.5
# homeassistant.components.ambient_network # homeassistant.components.ambient_network
# homeassistant.components.ambient_station # homeassistant.components.ambient_station
@ -798,7 +798,7 @@ gardena-bluetooth==1.4.3
gassist-text==0.0.11 gassist-text==0.0.11
# homeassistant.components.google # homeassistant.components.google
gcal-sync==6.1.5 gcal-sync==6.1.6
# homeassistant.components.geniushub # homeassistant.components.geniushub
geniushub-client==0.7.1 geniushub-client==0.7.1
@ -1448,7 +1448,7 @@ pybalboa==1.0.2
pyblackbird==0.6 pyblackbird==0.6
# homeassistant.components.bluesound # homeassistant.components.bluesound
pyblu==1.0.3 pyblu==1.0.4
# homeassistant.components.neato # homeassistant.components.neato
pybotvac==0.0.25 pybotvac==0.0.25
@ -1574,7 +1574,7 @@ pyinsteon==1.6.3
pyipma==3.0.7 pyipma==3.0.7
# homeassistant.components.ipp # homeassistant.components.ipp
pyipp==0.16.0 pyipp==0.17.0
# homeassistant.components.iqvia # homeassistant.components.iqvia
pyiqvia==2022.04.0 pyiqvia==2022.04.0
@ -1703,7 +1703,7 @@ pyopnsense==0.4.0
pyosoenergyapi==1.1.4 pyosoenergyapi==1.1.4
# homeassistant.components.opentherm_gw # homeassistant.components.opentherm_gw
pyotgw==2.2.1 pyotgw==2.2.2
# homeassistant.auth.mfa_modules.notify # homeassistant.auth.mfa_modules.notify
# homeassistant.auth.mfa_modules.totp # homeassistant.auth.mfa_modules.totp
@ -2122,7 +2122,7 @@ snapcast==2.3.6
soco==0.30.4 soco==0.30.4
# homeassistant.components.solarlog # homeassistant.components.solarlog
solarlog_cli==0.3.1 solarlog_cli==0.3.2
# homeassistant.components.solax # homeassistant.components.solax
solax==3.1.1 solax==3.1.1
@ -2415,7 +2415,7 @@ youless-api==2.1.2
youtubeaio==1.1.5 youtubeaio==1.1.5
# homeassistant.components.media_extractor # homeassistant.components.media_extractor
yt-dlp==2024.09.27 yt-dlp==2024.10.07
# homeassistant.components.zamg # homeassistant.components.zamg
zamg==0.3.6 zamg==0.3.6

View file

@ -220,6 +220,45 @@
}), }),
]), ]),
}), }),
dict({
'data': list([
dict({
'air_demand': 0,
'coldStage': 0,
'coldStages': 0,
'coolmaxtemp': 30,
'coolmintemp': 15,
'coolsetpoint': 20,
'errors': list([
]),
'floor_demand': 0,
'heatStage': 0,
'heatStages': 0,
'heatmaxtemp': 30,
'heatmintemp': 15,
'heatsetpoint': 20,
'humidity': 0,
'maxTemp': 30,
'minTemp': 15,
'mode': 6,
'modes': list([
1,
2,
3,
4,
5,
6,
]),
'name': 'Aux Heat',
'on': 1,
'roomTemp': 22,
'setpoint': 20,
'systemID': 4,
'units': 0,
'zoneID': 1,
}),
]),
}),
]), ]),
}), }),
'version': dict({ 'version': dict({
@ -269,8 +308,8 @@
'temp-set': 45, 'temp-set': 45,
'temp-unit': 0, 'temp-unit': 0,
}), }),
'num-systems': 3, 'num-systems': 4,
'num-zones': 7, 'num-zones': 8,
'systems': dict({ 'systems': dict({
'1': dict({ '1': dict({
'available': True, 'available': True,
@ -320,6 +359,23 @@
]), ]),
'problems': False, 'problems': False,
}), }),
'4': dict({
'available': True,
'full-name': 'Airzone [4] System',
'id': 4,
'master-system-zone': '4:1',
'master-zone': 1,
'mode': 6,
'modes': list([
1,
2,
3,
4,
5,
6,
]),
'problems': False,
}),
}), }),
'version': '1.62', 'version': '1.62',
'webserver': dict({ 'webserver': dict({
@ -683,6 +739,46 @@
'temp-step': 1.0, 'temp-step': 1.0,
'temp-unit': 1, 'temp-unit': 1,
}), }),
'4:1': dict({
'absolute-temp-max': 30.0,
'absolute-temp-min': 15.0,
'action': 5,
'air-demand': False,
'available': True,
'cold-stage': 0,
'cool-temp-max': 30.0,
'cool-temp-min': 15.0,
'cool-temp-set': 20.0,
'demand': False,
'double-set-point': False,
'floor-demand': False,
'full-name': 'Airzone [4:1] Aux Heat',
'heat-stage': 0,
'heat-temp-max': 30.0,
'heat-temp-min': 15.0,
'heat-temp-set': 20.0,
'id': 1,
'master': True,
'mode': 6,
'modes': list([
1,
2,
3,
4,
5,
6,
]),
'name': 'Aux Heat',
'on': True,
'problems': False,
'system': 4,
'temp': 22.0,
'temp-max': 30.0,
'temp-min': 15.0,
'temp-set': 20.0,
'temp-step': 0.5,
'temp-unit': 0,
}),
}), }),
}), }),
}) })

View file

@ -225,6 +225,23 @@ async def test_airzone_create_climates(hass: HomeAssistant) -> None:
assert state.attributes.get(ATTR_TARGET_TEMP_HIGH) == 25.0 assert state.attributes.get(ATTR_TARGET_TEMP_HIGH) == 25.0
assert state.attributes.get(ATTR_TARGET_TEMP_LOW) == 22.8 assert state.attributes.get(ATTR_TARGET_TEMP_LOW) == 22.8
state = hass.states.get("climate.aux_heat")
assert state.state == HVACMode.HEAT
assert state.attributes.get(ATTR_CURRENT_HUMIDITY) is None
assert state.attributes.get(ATTR_CURRENT_TEMPERATURE) == 22
assert state.attributes.get(ATTR_HVAC_ACTION) == HVACAction.IDLE
assert state.attributes.get(ATTR_HVAC_MODES) == [
HVACMode.OFF,
HVACMode.COOL,
HVACMode.HEAT,
HVACMode.FAN_ONLY,
HVACMode.DRY,
]
assert state.attributes.get(ATTR_MAX_TEMP) == 30
assert state.attributes.get(ATTR_MIN_TEMP) == 15
assert state.attributes.get(ATTR_TARGET_TEMP_STEP) == API_TEMPERATURE_STEP
assert state.attributes.get(ATTR_TEMPERATURE) == 20.0
HVAC_MOCK_CHANGED = copy.deepcopy(HVAC_MOCK) HVAC_MOCK_CHANGED = copy.deepcopy(HVAC_MOCK)
HVAC_MOCK_CHANGED[API_SYSTEMS][0][API_DATA][0][API_MAX_TEMP] = 25 HVAC_MOCK_CHANGED[API_SYSTEMS][0][API_DATA][0][API_MAX_TEMP] = 25
HVAC_MOCK_CHANGED[API_SYSTEMS][0][API_DATA][0][API_MIN_TEMP] = 10 HVAC_MOCK_CHANGED[API_SYSTEMS][0][API_DATA][0][API_MIN_TEMP] = 10

View file

@ -272,6 +272,37 @@ HVAC_MOCK = {
}, },
] ]
}, },
{
API_DATA: [
{
API_SYSTEM_ID: 4,
API_ZONE_ID: 1,
API_NAME: "Aux Heat",
API_ON: 1,
API_COOL_SET_POINT: 20,
API_COOL_MAX_TEMP: 30,
API_COOL_MIN_TEMP: 15,
API_HEAT_SET_POINT: 20,
API_HEAT_MAX_TEMP: 30,
API_HEAT_MIN_TEMP: 15,
API_MAX_TEMP: 30,
API_MIN_TEMP: 15,
API_SET_POINT: 20,
API_ROOM_TEMP: 22,
API_MODES: [1, 2, 3, 4, 5, 6],
API_MODE: 6,
API_COLD_STAGES: 0,
API_COLD_STAGE: 0,
API_HEAT_STAGES: 0,
API_HEAT_STAGE: 0,
API_HUMIDITY: 0,
API_UNITS: 0,
API_ERRORS: [],
API_AIR_DEMAND: 0,
API_FLOOR_DEMAND: 0,
},
]
},
] ]
} }

View file

@ -2,6 +2,7 @@
# name: test_diagnostics # name: test_diagnostics
dict({ dict({
'data': dict({ 'data': dict({
'booted_at': '2019-11-11T09:10:02+00:00',
'info': dict({ 'info': dict({
'command_set': 'ESCPL2,BDC,D4,D4PX,ESCPR7,END4,GENEP,URF', 'command_set': 'ESCPL2,BDC,D4,D4PX,ESCPR7,END4,GENEP,URF',
'location': None, 'location': None,

View file

@ -1,5 +1,6 @@
"""Tests for the diagnostics data provided by the Internet Printing Protocol (IPP) integration.""" """Tests for the diagnostics data provided by the Internet Printing Protocol (IPP) integration."""
import pytest
from syrupy import SnapshotAssertion from syrupy import SnapshotAssertion
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
@ -9,6 +10,7 @@ from tests.components.diagnostics import get_diagnostics_for_config_entry
from tests.typing import ClientSessionGenerator from tests.typing import ClientSessionGenerator
@pytest.mark.freeze_time("2019-11-11 09:10:32+00:00")
async def test_diagnostics( async def test_diagnostics(
hass: HomeAssistant, hass: HomeAssistant,
hass_client: ClientSessionGenerator, hass_client: ClientSessionGenerator,

View file

@ -6,7 +6,7 @@ from unittest.mock import MagicMock
import pytest import pytest
from rokuecp import RokuConnectionError from rokuecp import RokuConnectionError
from homeassistant.components.roku.const import DOMAIN from homeassistant.components.roku.const import CONF_PLAY_MEDIA_APP_ID, DOMAIN
from homeassistant.config_entries import SOURCE_HOMEKIT, SOURCE_SSDP, SOURCE_USER from homeassistant.config_entries import SOURCE_HOMEKIT, SOURCE_SSDP, SOURCE_USER
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_SOURCE from homeassistant.const import CONF_HOST, CONF_NAME, CONF_SOURCE
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
@ -254,3 +254,25 @@ async def test_ssdp_discovery(
assert result["data"] assert result["data"]
assert result["data"][CONF_HOST] == HOST assert result["data"][CONF_HOST] == HOST
assert result["data"][CONF_NAME] == UPNP_FRIENDLY_NAME assert result["data"][CONF_NAME] == UPNP_FRIENDLY_NAME
async def test_options_flow(
hass: HomeAssistant, mock_config_entry: MockConfigEntry
) -> None:
"""Test options config flow."""
mock_config_entry.add_to_hass(hass)
result = await hass.config_entries.options.async_init(mock_config_entry.entry_id)
assert result.get("type") is FlowResultType.FORM
assert result.get("step_id") == "init"
result2 = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={CONF_PLAY_MEDIA_APP_ID: "782875"},
)
assert result2.get("type") is FlowResultType.CREATE_ENTRY
assert result2.get("data") == {
CONF_PLAY_MEDIA_APP_ID: "782875",
}

View file

@ -32,6 +32,7 @@ from homeassistant.components.roku.const import (
ATTR_FORMAT, ATTR_FORMAT,
ATTR_KEYWORD, ATTR_KEYWORD,
ATTR_MEDIA_TYPE, ATTR_MEDIA_TYPE,
DEFAULT_PLAY_MEDIA_APP_ID,
DOMAIN, DOMAIN,
SERVICE_SEARCH, SERVICE_SEARCH,
) )
@ -495,7 +496,7 @@ async def test_services_play_media(
blocking=True, blocking=True,
) )
assert mock_roku.play_on_roku.call_count == 0 assert mock_roku.launch.call_count == 0
await hass.services.async_call( await hass.services.async_call(
MP_DOMAIN, MP_DOMAIN,
@ -509,7 +510,7 @@ async def test_services_play_media(
blocking=True, blocking=True,
) )
assert mock_roku.play_on_roku.call_count == 0 assert mock_roku.launch.call_count == 0
@pytest.mark.parametrize( @pytest.mark.parametrize(
@ -546,9 +547,10 @@ async def test_services_play_media_audio(
}, },
blocking=True, blocking=True,
) )
mock_roku.play_on_roku.assert_called_once_with( mock_roku.launch.assert_called_once_with(
content_id, DEFAULT_PLAY_MEDIA_APP_ID,
{ {
"u": content_id,
"t": "a", "t": "a",
"songName": resolved_name, "songName": resolved_name,
"songFormat": resolved_format, "songFormat": resolved_format,
@ -591,9 +593,11 @@ async def test_services_play_media_video(
}, },
blocking=True, blocking=True,
) )
mock_roku.play_on_roku.assert_called_once_with( mock_roku.launch.assert_called_once_with(
content_id, DEFAULT_PLAY_MEDIA_APP_ID,
{ {
"u": content_id,
"t": "v",
"videoName": resolved_name, "videoName": resolved_name,
"videoFormat": resolved_format, "videoFormat": resolved_format,
}, },
@ -617,10 +621,12 @@ async def test_services_camera_play_stream(
blocking=True, blocking=True,
) )
assert mock_roku.play_on_roku.call_count == 1 assert mock_roku.launch.call_count == 1
mock_roku.play_on_roku.assert_called_with( mock_roku.launch.assert_called_with(
"https://awesome.tld/api/hls/api_token/master_playlist.m3u8", DEFAULT_PLAY_MEDIA_APP_ID,
{ {
"u": "https://awesome.tld/api/hls/api_token/master_playlist.m3u8",
"t": "v",
"videoName": "Camera Stream", "videoName": "Camera Stream",
"videoFormat": "hls", "videoFormat": "hls",
}, },
@ -653,14 +659,21 @@ async def test_services_play_media_local_source(
blocking=True, blocking=True,
) )
assert mock_roku.play_on_roku.call_count == 1 assert mock_roku.launch.call_count == 1
assert mock_roku.play_on_roku.call_args assert mock_roku.launch.call_args
call_args = mock_roku.play_on_roku.call_args.args call_args = mock_roku.launch.call_args.args
assert "/local/Epic%20Sax%20Guy%2010%20Hours.mp4?authSig=" in call_args[0] assert call_args[0] == DEFAULT_PLAY_MEDIA_APP_ID
assert call_args[1] == { assert "u" in call_args[1]
"videoFormat": "mp4", assert "/local/Epic%20Sax%20Guy%2010%20Hours.mp4?authSig=" in call_args[1]["u"]
"videoName": "media-source://media_source/local/Epic Sax Guy 10 Hours.mp4", assert "t" in call_args[1]
} assert call_args[1]["t"] == "v"
assert "videoFormat" in call_args[1]
assert call_args[1]["videoFormat"] == "mp4"
assert "videoName" in call_args[1]
assert (
call_args[1]["videoName"]
== "media-source://media_source/local/Epic Sax Guy 10 Hours.mp4"
)
@pytest.mark.parametrize("mock_device", ["roku/rokutv-7820x.json"], indirect=True) @pytest.mark.parametrize("mock_device", ["roku/rokutv-7820x.json"], indirect=True)

View file

@ -294,6 +294,10 @@ async def test_entity_created_and_removed(
assert item["id"] == "1234567890" assert item["id"] == "1234567890"
assert item["name"] == "Kitchen tag" assert item["name"] == "Kitchen tag"
await hass.async_block_till_done()
er_entity = entity_registry.async_get("tag.kitchen_tag")
assert er_entity.name == "Kitchen tag"
entity = hass.states.get("tag.kitchen_tag") entity = hass.states.get("tag.kitchen_tag")
assert entity assert entity
assert entity.state == STATE_UNKNOWN assert entity.state == STATE_UNKNOWN

View file

@ -112,6 +112,96 @@ async def test_config_flow_from_dhcp_add_mac(
assert hass.config_entries.async_entries(DOMAIN)[0].unique_id == "00:11:22:33:44:55" assert hass.config_entries.async_entries(DOMAIN)[0].unique_id == "00:11:22:33:44:55"
async def test_config_flow_from_dhcp_ip_update(
hass: HomeAssistant,
mock_setup_entry: AsyncMock,
) -> None:
"""Test we can use DHCP discovery to update IP in a config entry."""
info = DhcpServiceInfo(
ip="1.2.3.4", hostname="webcontrol", macaddress="00:11:22:33:44:55"
)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_DHCP}, data=info
)
assert result["type"] is FlowResultType.FORM
assert result["errors"] == {}
with patch(
"wmspro.webcontrol.WebControlPro.ping",
return_value=True,
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_HOST: "1.2.3.4",
},
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == "1.2.3.4"
assert result["data"] == {
CONF_HOST: "1.2.3.4",
}
assert len(mock_setup_entry.mock_calls) == 1
assert hass.config_entries.async_entries(DOMAIN)[0].unique_id == "00:11:22:33:44:55"
info = DhcpServiceInfo(
ip="5.6.7.8", hostname="webcontrol", macaddress="00:11:22:33:44:55"
)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_DHCP}, data=info
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "already_configured"
assert hass.config_entries.async_entries(DOMAIN)[0].unique_id == "00:11:22:33:44:55"
assert hass.config_entries.async_entries(DOMAIN)[0].data[CONF_HOST] == "5.6.7.8"
async def test_config_flow_from_dhcp_no_update(
hass: HomeAssistant,
mock_setup_entry: AsyncMock,
) -> None:
"""Test we do not use DHCP discovery to overwrite hostname with IP in config entry."""
info = DhcpServiceInfo(
ip="1.2.3.4", hostname="webcontrol", macaddress="00:11:22:33:44:55"
)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_DHCP}, data=info
)
assert result["type"] is FlowResultType.FORM
assert result["errors"] == {}
with patch(
"wmspro.webcontrol.WebControlPro.ping",
return_value=True,
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_HOST: "webcontrol",
},
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == "webcontrol"
assert result["data"] == {
CONF_HOST: "webcontrol",
}
assert len(mock_setup_entry.mock_calls) == 1
assert hass.config_entries.async_entries(DOMAIN)[0].unique_id == "00:11:22:33:44:55"
info = DhcpServiceInfo(
ip="5.6.7.8", hostname="webcontrol", macaddress="00:11:22:33:44:55"
)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_DHCP}, data=info
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "already_configured"
assert hass.config_entries.async_entries(DOMAIN)[0].unique_id == "00:11:22:33:44:55"
assert hass.config_entries.async_entries(DOMAIN)[0].data[CONF_HOST] == "webcontrol"
async def test_config_flow_ping_failed( async def test_config_flow_ping_failed(
hass: HomeAssistant, mock_setup_entry: AsyncMock hass: HomeAssistant, mock_setup_entry: AsyncMock
) -> None: ) -> None:

View file

@ -812,8 +812,8 @@ async def test_thermostat_heatit_z_trm2fx(
| ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_OFF
| ClimateEntityFeature.TURN_ON | ClimateEntityFeature.TURN_ON
) )
assert state.attributes[ATTR_MIN_TEMP] == 7 assert state.attributes[ATTR_MIN_TEMP] == 0
assert state.attributes[ATTR_MAX_TEMP] == 35 assert state.attributes[ATTR_MAX_TEMP] == 50
# Try switching to external sensor # Try switching to external sensor
event = Event( event = Event(