diff --git a/homeassistant/components/airzone/climate.py b/homeassistant/components/airzone/climate.py index 5e5e1c126dedfc..559513d34399d2 100644 --- a/homeassistant/components/airzone/climate.py +++ b/homeassistant/components/airzone/climate.py @@ -85,6 +85,7 @@ OperationMode.HEATING: HVACMode.HEAT, OperationMode.FAN: HVACMode.FAN_ONLY, OperationMode.DRY: HVACMode.DRY, + OperationMode.AUX_HEATING: HVACMode.HEAT, OperationMode.AUTO: HVACMode.HEAT_COOL, } HVAC_MODE_HASS_TO_LIB: Final[dict[HVACMode, OperationMode]] = { @@ -157,9 +158,10 @@ def __init__( self._attr_temperature_unit = TEMP_UNIT_LIB_TO_HASS[ 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) ] + self._attr_hvac_modes = list(dict.fromkeys(_attr_hvac_modes)) if ( self.get_airzone_value(AZD_SPEED) is not None and self.get_airzone_value(AZD_SPEEDS) is not None diff --git a/homeassistant/components/airzone/manifest.json b/homeassistant/components/airzone/manifest.json index c40f4138b0ad3e..10fb20bb2ce9f5 100644 --- a/homeassistant/components/airzone/manifest.json +++ b/homeassistant/components/airzone/manifest.json @@ -11,5 +11,5 @@ "documentation": "https://www.home-assistant.io/integrations/airzone", "iot_class": "local_polling", "loggers": ["aioairzone"], - "requirements": ["aioairzone==0.9.3"] + "requirements": ["aioairzone==0.9.5"] } diff --git a/homeassistant/components/bluesound/manifest.json b/homeassistant/components/bluesound/manifest.json index 4d92a5f7fc06ec..462112a8b783c7 100644 --- a/homeassistant/components/bluesound/manifest.json +++ b/homeassistant/components/bluesound/manifest.json @@ -6,7 +6,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/bluesound", "iot_class": "local_polling", - "requirements": ["pyblu==1.0.3"], + "requirements": ["pyblu==1.0.4"], "zeroconf": [ { "type": "_musc._tcp.local." diff --git a/homeassistant/components/bluesound/media_player.py b/homeassistant/components/bluesound/media_player.py index 1e2a537cd62e67..1a633468a3a4fa 100644 --- a/homeassistant/components/bluesound/media_player.py +++ b/homeassistant/components/bluesound/media_player.py @@ -493,6 +493,8 @@ def media_position(self) -> int | None: return None position = self._status.seconds + if position is None: + return None if mediastate == MediaPlayerState.PLAYING: position += (dt_util.utcnow() - self._last_status_update).total_seconds() diff --git a/homeassistant/components/daikin/climate.py b/homeassistant/components/daikin/climate.py index f1fc047311504d..39e92ab19216d6 100644 --- a/homeassistant/components/daikin/climate.py +++ b/homeassistant/components/daikin/climate.py @@ -159,6 +159,7 @@ async def _set(self, settings: dict[str, Any]) -> None: if values: await self.device.set(values) + await self.coordinator.async_refresh() @property def unique_id(self) -> str: @@ -261,6 +262,7 @@ async def async_set_preset_mode(self, preset_mode: str) -> None: await self.device.set_advanced_mode( HA_PRESET_TO_DAIKIN[PRESET_ECO], ATTR_STATE_OFF ) + await self.coordinator.async_refresh() @property def preset_modes(self) -> list[str]: @@ -275,9 +277,11 @@ def preset_modes(self) -> list[str]: async def async_turn_on(self) -> None: """Turn device on.""" await self.device.set({}) + await self.coordinator.async_refresh() async def async_turn_off(self) -> None: """Turn device off.""" await self.device.set( {HA_ATTR_TO_DAIKIN[ATTR_HVAC_MODE]: HA_STATE_TO_DAIKIN[HVACMode.OFF]} ) + await self.coordinator.async_refresh() diff --git a/homeassistant/components/daikin/switch.py b/homeassistant/components/daikin/switch.py index 23517d085d2f3d..669048ac45e1da 100644 --- a/homeassistant/components/daikin/switch.py +++ b/homeassistant/components/daikin/switch.py @@ -63,10 +63,12 @@ def is_on(self) -> bool: async def async_turn_on(self, **kwargs: Any) -> None: """Turn the zone on.""" 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: """Turn the zone off.""" await self.device.set_zone(self._zone_id, "zone_onoff", "0") + await self.coordinator.async_refresh() class DaikinStreamerSwitch(DaikinEntity, SwitchEntity): @@ -88,10 +90,12 @@ def is_on(self) -> bool: async def async_turn_on(self, **kwargs: Any) -> None: """Turn the zone on.""" await self.device.set_streamer("on") + await self.coordinator.async_refresh() async def async_turn_off(self, **kwargs: Any) -> None: """Turn the zone off.""" await self.device.set_streamer("off") + await self.coordinator.async_refresh() class DaikinToggleSwitch(DaikinEntity, SwitchEntity): @@ -112,7 +116,9 @@ def is_on(self) -> bool: async def async_turn_on(self, **kwargs: Any) -> None: """Turn the zone on.""" await self.device.set({}) + await self.coordinator.async_refresh() async def async_turn_off(self, **kwargs: Any) -> None: """Turn the zone off.""" await self.device.set({DAIKIN_ATTR_MODE: "off"}) + await self.coordinator.async_refresh() diff --git a/homeassistant/components/google/manifest.json b/homeassistant/components/google/manifest.json index 288ccbd6899df7..0245333d713391 100644 --- a/homeassistant/components/google/manifest.json +++ b/homeassistant/components/google/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/calendar.google", "iot_class": "cloud_polling", "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"] } diff --git a/homeassistant/components/ipp/manifest.json b/homeassistant/components/ipp/manifest.json index 2ba82b2cfecef0..baa41cf00bd0d4 100644 --- a/homeassistant/components/ipp/manifest.json +++ b/homeassistant/components/ipp/manifest.json @@ -8,6 +8,6 @@ "iot_class": "local_polling", "loggers": ["deepmerge", "pyipp"], "quality_scale": "platinum", - "requirements": ["pyipp==0.16.0"], + "requirements": ["pyipp==0.17.0"], "zeroconf": ["_ipps._tcp.local.", "_ipp._tcp.local."] } diff --git a/homeassistant/components/ipp/sensor.py b/homeassistant/components/ipp/sensor.py index e872fc7977f37f..a2792c7749b8cf 100644 --- a/homeassistant/components/ipp/sensor.py +++ b/homeassistant/components/ipp/sensor.py @@ -4,7 +4,7 @@ from collections.abc import Callable from dataclasses import dataclass -from datetime import datetime, timedelta +from datetime import datetime from typing import Any from pyipp import Marker, Printer @@ -19,7 +19,6 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType -from homeassistant.util.dt import utcnow from . import IPPConfigEntry from .const import ( @@ -80,7 +79,7 @@ def _get_marker_value_fn( device_class=SensorDeviceClass.TIMESTAMP, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, - value_fn=lambda printer: (utcnow() - timedelta(seconds=printer.info.uptime)), + value_fn=lambda printer: printer.booted_at, ), ) diff --git a/homeassistant/components/media_extractor/manifest.json b/homeassistant/components/media_extractor/manifest.json index 635ab5f6d40744..fa7657244d61b7 100644 --- a/homeassistant/components/media_extractor/manifest.json +++ b/homeassistant/components/media_extractor/manifest.json @@ -8,6 +8,6 @@ "iot_class": "calculated", "loggers": ["yt_dlp"], "quality_scale": "internal", - "requirements": ["yt-dlp==2024.09.27"], + "requirements": ["yt-dlp==2024.10.07"], "single_config_entry": true } diff --git a/homeassistant/components/opentherm_gw/manifest.json b/homeassistant/components/opentherm_gw/manifest.json index 927f9c9ca3e1a7..ecd0a6b99d5d73 100644 --- a/homeassistant/components/opentherm_gw/manifest.json +++ b/homeassistant/components/opentherm_gw/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/opentherm_gw", "iot_class": "local_push", "loggers": ["pyotgw"], - "requirements": ["pyotgw==2.2.1"] + "requirements": ["pyotgw==2.2.2"] } diff --git a/homeassistant/components/roku/__init__.py b/homeassistant/components/roku/__init__.py index 7515f375054227..b318a91e4c7f65 100644 --- a/homeassistant/components/roku/__init__.py +++ b/homeassistant/components/roku/__init__.py @@ -6,7 +6,7 @@ from homeassistant.const import CONF_HOST, Platform 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 PLATFORMS = [ @@ -24,7 +24,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: device_id = entry.entry_id 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() @@ -32,6 +37,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + entry.async_on_unload(entry.add_update_listener(async_reload_entry)) + 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): hass.data[DOMAIN].pop(entry.entry_id) 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) diff --git a/homeassistant/components/roku/config_flow.py b/homeassistant/components/roku/config_flow.py index 7757cc53e1cf41..3ece9aff3f2baf 100644 --- a/homeassistant/components/roku/config_flow.py +++ b/homeassistant/components/roku/config_flow.py @@ -10,12 +10,17 @@ import voluptuous as vol 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.core import HomeAssistant, callback 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}) @@ -155,3 +160,36 @@ async def async_step_discovery_confirm( title=self.discovery_info[CONF_NAME], 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, + } + ), + ) diff --git a/homeassistant/components/roku/const.py b/homeassistant/components/roku/const.py index ab633a4044cf8d..f0c7d4e253778e 100644 --- a/homeassistant/components/roku/const.py +++ b/homeassistant/components/roku/const.py @@ -15,3 +15,9 @@ # Services SERVICE_SEARCH = "search" + +# Config +CONF_PLAY_MEDIA_APP_ID = "play_media_app_id" + +# Defaults +DEFAULT_PLAY_MEDIA_APP_ID = "15985" diff --git a/homeassistant/components/roku/coordinator.py b/homeassistant/components/roku/coordinator.py index 303d0e91a36056..7900669d02f8ac 100644 --- a/homeassistant/components/roku/coordinator.py +++ b/homeassistant/components/roku/coordinator.py @@ -29,15 +29,12 @@ class RokuDataUpdateCoordinator(DataUpdateCoordinator[Device]): roku: Roku def __init__( - self, - hass: HomeAssistant, - *, - host: str, - device_id: str, + self, hass: HomeAssistant, *, host: str, device_id: str, play_media_app_id: str ) -> None: """Initialize global Roku data updater.""" self.device_id = device_id 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.last_full_update = None diff --git a/homeassistant/components/roku/media_player.py b/homeassistant/components/roku/media_player.py index 5b15253068e051..35f01553cdd168 100644 --- a/homeassistant/components/roku/media_player.py +++ b/homeassistant/components/roku/media_player.py @@ -445,17 +445,25 @@ async def async_play_media( 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}: params = { param: extra[attr] for (attr, param) in ATTRS_TO_PLAY_ON_ROKU_PARAMS.items() 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: _LOGGER.error("Media type %s is not supported", original_media_type) return diff --git a/homeassistant/components/roku/strings.json b/homeassistant/components/roku/strings.json index 9eef366163eef1..9d657be6d61f56 100644 --- a/homeassistant/components/roku/strings.json +++ b/homeassistant/components/roku/strings.json @@ -24,6 +24,18 @@ "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": { "binary_sensor": { "headphones_connected": { diff --git a/homeassistant/components/solarlog/manifest.json b/homeassistant/components/solarlog/manifest.json index 274c97c76b52de..9f80b749d08649 100644 --- a/homeassistant/components/solarlog/manifest.json +++ b/homeassistant/components/solarlog/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/solarlog", "iot_class": "local_polling", "loggers": ["solarlog_cli"], - "requirements": ["solarlog_cli==0.3.1"] + "requirements": ["solarlog_cli==0.3.2"] } diff --git a/homeassistant/components/tag/__init__.py b/homeassistant/components/tag/__init__.py index 0462c5bec3488e..95efae3d386b7a 100644 --- a/homeassistant/components/tag/__init__.py +++ b/homeassistant/components/tag/__init__.py @@ -84,7 +84,9 @@ def _create_entry( original_name=f"{DEFAULT_NAME} {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]): diff --git a/homeassistant/components/wmspro/config_flow.py b/homeassistant/components/wmspro/config_flow.py index 19b9ab28e6af61..c28cf5efce303f 100644 --- a/homeassistant/components/wmspro/config_flow.py +++ b/homeassistant/components/wmspro/config_flow.py @@ -2,6 +2,7 @@ from __future__ import annotations +import ipaddress import logging from typing import Any @@ -38,7 +39,19 @@ async def async_step_dhcp( """Handle the DHCP discovery step.""" unique_id = format_mac(discovery_info.macaddress) 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): if not entry.unique_id and entry.data[CONF_HOST] in ( diff --git a/homeassistant/components/zha/helpers.py b/homeassistant/components/zha/helpers.py index b91565835a72a6..e8d53ac11ad620 100644 --- a/homeassistant/components/zha/helpers.py +++ b/homeassistant/components/zha/helpers.py @@ -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 # pass it to the ZHA library 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, hass.config.path(DEFAULT_DATABASE_NAME), ) diff --git a/homeassistant/components/zwave_js/climate.py b/homeassistant/components/zwave_js/climate.py index 14a3fe579c4899..c7ab579c2cb28a 100644 --- a/homeassistant/components/zwave_js/climate.py +++ b/homeassistant/components/zwave_js/climate.py @@ -24,8 +24,6 @@ ATTR_HVAC_MODE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, - DEFAULT_MAX_TEMP, - DEFAULT_MIN_TEMP, DOMAIN as CLIMATE_DOMAIN, PRESET_NONE, ClimateEntity, @@ -421,7 +419,7 @@ def extra_state_attributes(self) -> dict[str, str] | None: @property def min_temp(self) -> float: """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 try: temp = self._setpoint_value_or_raise(self._current_mode_setpoint_enums[0]) @@ -437,7 +435,7 @@ def min_temp(self) -> float: @property def max_temp(self) -> float: """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 try: temp = self._setpoint_value_or_raise(self._current_mode_setpoint_enums[0]) diff --git a/homeassistant/const.py b/homeassistant/const.py index b539cbc6068fd0..62835ef723b1a5 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -24,7 +24,7 @@ APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 10 -PATCH_VERSION: Final = "2" +PATCH_VERSION: Final = "3" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 159463e8928510..a05c932b0f1e6e 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -31,7 +31,7 @@ ha-ffmpeg==3.2.0 habluetooth==3.4.0 hass-nabucasa==0.81.1 hassil==1.7.4 -home-assistant-bluetooth==1.12.2 +home-assistant-bluetooth==1.13.0 home-assistant-frontend==20241002.3 home-assistant-intents==2024.10.2 httpx==0.27.2 diff --git a/pyproject.toml b/pyproject.toml index a79ffb0fe57b2f..dd50e28be98c51 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.10.2" +version = "2024.10.3" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" @@ -46,7 +46,7 @@ dependencies = [ # When bumping httpx, please check the version pins of # httpcore, anyio, and h11 in gen_requirements_all "httpx==0.27.2", - "home-assistant-bluetooth==1.12.2", + "home-assistant-bluetooth==1.13.0", "ifaddr==0.2.0", "Jinja2==3.1.4", "lru-dict==1.3.0", diff --git a/requirements.txt b/requirements.txt index 98ba315294bf5e..c15e23553d5aa9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -20,7 +20,7 @@ ciso8601==2.3.1 fnv-hash-fast==1.0.2 hass-nabucasa==0.81.1 httpx==0.27.2 -home-assistant-bluetooth==1.12.2 +home-assistant-bluetooth==1.13.0 ifaddr==0.2.0 Jinja2==3.1.4 lru-dict==1.3.0 diff --git a/requirements_all.txt b/requirements_all.txt index 4818ded19dc852..1400ed47f10e26 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -179,7 +179,7 @@ aioairq==0.3.2 aioairzone-cloud==0.6.6 # homeassistant.components.airzone -aioairzone==0.9.3 +aioairzone==0.9.5 # homeassistant.components.ambient_network # homeassistant.components.ambient_station @@ -945,7 +945,7 @@ gardena-bluetooth==1.4.3 gassist-text==0.0.11 # homeassistant.components.google -gcal-sync==6.1.5 +gcal-sync==6.1.6 # homeassistant.components.geniushub geniushub-client==0.7.1 @@ -1780,7 +1780,7 @@ pybbox==0.0.5-alpha pyblackbird==0.6 # homeassistant.components.bluesound -pyblu==1.0.3 +pyblu==1.0.4 # homeassistant.components.neato pybotvac==0.0.25 @@ -1960,7 +1960,7 @@ pyintesishome==1.8.0 pyipma==3.0.7 # homeassistant.components.ipp -pyipp==0.16.0 +pyipp==0.17.0 # homeassistant.components.iqvia pyiqvia==2022.04.0 @@ -2119,7 +2119,7 @@ pyoppleio-legacy==1.0.8 pyosoenergyapi==1.1.4 # homeassistant.components.opentherm_gw -pyotgw==2.2.1 +pyotgw==2.2.2 # homeassistant.auth.mfa_modules.notify # homeassistant.auth.mfa_modules.totp @@ -2676,7 +2676,7 @@ soco==0.30.4 solaredge-local==0.2.3 # homeassistant.components.solarlog -solarlog_cli==0.3.1 +solarlog_cli==0.3.2 # homeassistant.components.solax solax==3.1.1 @@ -3032,7 +3032,7 @@ youless-api==2.1.2 youtubeaio==1.1.5 # homeassistant.components.media_extractor -yt-dlp==2024.09.27 +yt-dlp==2024.10.07 # homeassistant.components.zamg zamg==0.3.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ed42a88ef621a5..dc41fc22d5a60c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -167,7 +167,7 @@ aioairq==0.3.2 aioairzone-cloud==0.6.6 # homeassistant.components.airzone -aioairzone==0.9.3 +aioairzone==0.9.5 # homeassistant.components.ambient_network # homeassistant.components.ambient_station @@ -798,7 +798,7 @@ gardena-bluetooth==1.4.3 gassist-text==0.0.11 # homeassistant.components.google -gcal-sync==6.1.5 +gcal-sync==6.1.6 # homeassistant.components.geniushub geniushub-client==0.7.1 @@ -1448,7 +1448,7 @@ pybalboa==1.0.2 pyblackbird==0.6 # homeassistant.components.bluesound -pyblu==1.0.3 +pyblu==1.0.4 # homeassistant.components.neato pybotvac==0.0.25 @@ -1574,7 +1574,7 @@ pyinsteon==1.6.3 pyipma==3.0.7 # homeassistant.components.ipp -pyipp==0.16.0 +pyipp==0.17.0 # homeassistant.components.iqvia pyiqvia==2022.04.0 @@ -1703,7 +1703,7 @@ pyopnsense==0.4.0 pyosoenergyapi==1.1.4 # homeassistant.components.opentherm_gw -pyotgw==2.2.1 +pyotgw==2.2.2 # homeassistant.auth.mfa_modules.notify # homeassistant.auth.mfa_modules.totp @@ -2122,7 +2122,7 @@ snapcast==2.3.6 soco==0.30.4 # homeassistant.components.solarlog -solarlog_cli==0.3.1 +solarlog_cli==0.3.2 # homeassistant.components.solax solax==3.1.1 @@ -2415,7 +2415,7 @@ youless-api==2.1.2 youtubeaio==1.1.5 # homeassistant.components.media_extractor -yt-dlp==2024.09.27 +yt-dlp==2024.10.07 # homeassistant.components.zamg zamg==0.3.6 diff --git a/tests/components/airzone/snapshots/test_diagnostics.ambr b/tests/components/airzone/snapshots/test_diagnostics.ambr index 693550a3e1c4a4..fb4f6530b1e053 100644 --- a/tests/components/airzone/snapshots/test_diagnostics.ambr +++ b/tests/components/airzone/snapshots/test_diagnostics.ambr @@ -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({ @@ -269,8 +308,8 @@ 'temp-set': 45, 'temp-unit': 0, }), - 'num-systems': 3, - 'num-zones': 7, + 'num-systems': 4, + 'num-zones': 8, 'systems': dict({ '1': dict({ 'available': True, @@ -320,6 +359,23 @@ ]), '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', 'webserver': dict({ @@ -683,6 +739,46 @@ 'temp-step': 1.0, '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, + }), }), }), }) diff --git a/tests/components/airzone/test_climate.py b/tests/components/airzone/test_climate.py index 0f23c151e0ef52..12a73a6a268dd4 100644 --- a/tests/components/airzone/test_climate.py +++ b/tests/components/airzone/test_climate.py @@ -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_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[API_SYSTEMS][0][API_DATA][0][API_MAX_TEMP] = 25 HVAC_MOCK_CHANGED[API_SYSTEMS][0][API_DATA][0][API_MIN_TEMP] = 10 diff --git a/tests/components/airzone/util.py b/tests/components/airzone/util.py index 2cdb7a9c6f9c1f..278663b7a9754e 100644 --- a/tests/components/airzone/util.py +++ b/tests/components/airzone/util.py @@ -272,6 +272,37 @@ }, ] }, + { + 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, + }, + ] + }, ] } diff --git a/tests/components/ipp/snapshots/test_diagnostics.ambr b/tests/components/ipp/snapshots/test_diagnostics.ambr index 98d0055c98209c..bd2564c5a4098d 100644 --- a/tests/components/ipp/snapshots/test_diagnostics.ambr +++ b/tests/components/ipp/snapshots/test_diagnostics.ambr @@ -2,6 +2,7 @@ # name: test_diagnostics dict({ 'data': dict({ + 'booted_at': '2019-11-11T09:10:02+00:00', 'info': dict({ 'command_set': 'ESCPL2,BDC,D4,D4PX,ESCPR7,END4,GENEP,URF', 'location': None, diff --git a/tests/components/ipp/test_diagnostics.py b/tests/components/ipp/test_diagnostics.py index 08446601e69c51..d78f066d788aee 100644 --- a/tests/components/ipp/test_diagnostics.py +++ b/tests/components/ipp/test_diagnostics.py @@ -1,5 +1,6 @@ """Tests for the diagnostics data provided by the Internet Printing Protocol (IPP) integration.""" +import pytest from syrupy import SnapshotAssertion from homeassistant.core import HomeAssistant @@ -9,6 +10,7 @@ from tests.typing import ClientSessionGenerator +@pytest.mark.freeze_time("2019-11-11 09:10:32+00:00") async def test_diagnostics( hass: HomeAssistant, hass_client: ClientSessionGenerator, diff --git a/tests/components/roku/test_config_flow.py b/tests/components/roku/test_config_flow.py index 3cf5627f342e98..7144c77cad9433 100644 --- a/tests/components/roku/test_config_flow.py +++ b/tests/components/roku/test_config_flow.py @@ -6,7 +6,7 @@ import pytest 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.const import CONF_HOST, CONF_NAME, CONF_SOURCE from homeassistant.core import HomeAssistant @@ -254,3 +254,25 @@ async def test_ssdp_discovery( assert result["data"] assert result["data"][CONF_HOST] == HOST 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", + } diff --git a/tests/components/roku/test_media_player.py b/tests/components/roku/test_media_player.py index 9aff8f581d7cfb..03b1999ae831b2 100644 --- a/tests/components/roku/test_media_player.py +++ b/tests/components/roku/test_media_player.py @@ -32,6 +32,7 @@ ATTR_FORMAT, ATTR_KEYWORD, ATTR_MEDIA_TYPE, + DEFAULT_PLAY_MEDIA_APP_ID, DOMAIN, SERVICE_SEARCH, ) @@ -495,7 +496,7 @@ async def test_services_play_media( blocking=True, ) - assert mock_roku.play_on_roku.call_count == 0 + assert mock_roku.launch.call_count == 0 await hass.services.async_call( MP_DOMAIN, @@ -509,7 +510,7 @@ async def test_services_play_media( blocking=True, ) - assert mock_roku.play_on_roku.call_count == 0 + assert mock_roku.launch.call_count == 0 @pytest.mark.parametrize( @@ -546,9 +547,10 @@ async def test_services_play_media_audio( }, blocking=True, ) - mock_roku.play_on_roku.assert_called_once_with( - content_id, + mock_roku.launch.assert_called_once_with( + DEFAULT_PLAY_MEDIA_APP_ID, { + "u": content_id, "t": "a", "songName": resolved_name, "songFormat": resolved_format, @@ -591,9 +593,11 @@ async def test_services_play_media_video( }, blocking=True, ) - mock_roku.play_on_roku.assert_called_once_with( - content_id, + mock_roku.launch.assert_called_once_with( + DEFAULT_PLAY_MEDIA_APP_ID, { + "u": content_id, + "t": "v", "videoName": resolved_name, "videoFormat": resolved_format, }, @@ -617,10 +621,12 @@ async def test_services_camera_play_stream( blocking=True, ) - assert mock_roku.play_on_roku.call_count == 1 - mock_roku.play_on_roku.assert_called_with( - "https://awesome.tld/api/hls/api_token/master_playlist.m3u8", + assert mock_roku.launch.call_count == 1 + mock_roku.launch.assert_called_with( + DEFAULT_PLAY_MEDIA_APP_ID, { + "u": "https://awesome.tld/api/hls/api_token/master_playlist.m3u8", + "t": "v", "videoName": "Camera Stream", "videoFormat": "hls", }, @@ -653,14 +659,21 @@ async def test_services_play_media_local_source( blocking=True, ) - assert mock_roku.play_on_roku.call_count == 1 - assert mock_roku.play_on_roku.call_args - call_args = mock_roku.play_on_roku.call_args.args - assert "/local/Epic%20Sax%20Guy%2010%20Hours.mp4?authSig=" in call_args[0] - assert call_args[1] == { - "videoFormat": "mp4", - "videoName": "media-source://media_source/local/Epic Sax Guy 10 Hours.mp4", - } + assert mock_roku.launch.call_count == 1 + assert mock_roku.launch.call_args + call_args = mock_roku.launch.call_args.args + assert call_args[0] == DEFAULT_PLAY_MEDIA_APP_ID + assert "u" in call_args[1] + assert "/local/Epic%20Sax%20Guy%2010%20Hours.mp4?authSig=" in call_args[1]["u"] + 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) diff --git a/tests/components/tag/test_init.py b/tests/components/tag/test_init.py index 6f309391d2b804..5c1e80c2d8bc87 100644 --- a/tests/components/tag/test_init.py +++ b/tests/components/tag/test_init.py @@ -294,6 +294,10 @@ async def test_entity_created_and_removed( assert item["id"] == "1234567890" 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") assert entity assert entity.state == STATE_UNKNOWN diff --git a/tests/components/wmspro/test_config_flow.py b/tests/components/wmspro/test_config_flow.py index 6a254a93836e7a..c25641a897908d 100644 --- a/tests/components/wmspro/test_config_flow.py +++ b/tests/components/wmspro/test_config_flow.py @@ -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" +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( hass: HomeAssistant, mock_setup_entry: AsyncMock ) -> None: diff --git a/tests/components/zwave_js/test_climate.py b/tests/components/zwave_js/test_climate.py index 9a4559de1a59df..5d711528a281ed 100644 --- a/tests/components/zwave_js/test_climate.py +++ b/tests/components/zwave_js/test_climate.py @@ -812,8 +812,8 @@ async def test_thermostat_heatit_z_trm2fx( | ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON ) - assert state.attributes[ATTR_MIN_TEMP] == 7 - assert state.attributes[ATTR_MAX_TEMP] == 35 + assert state.attributes[ATTR_MIN_TEMP] == 0 + assert state.attributes[ATTR_MAX_TEMP] == 50 # Try switching to external sensor event = Event(