Compare commits

..

35 Commits

Author SHA1 Message Date
Franck Nijhof
c09f15b0e9 Bump version to 2024.10.4 2024-10-25 20:49:36 +02:00
Joost Lekkerkerker
9a44d668d6 Bump nyt_games to 0.4.4 (#129152) 2024-10-25 20:43:16 +02:00
Joost Lekkerkerker
67e0197a7a Fix NYT Games connection max streak (#129149) 2024-10-25 20:43:09 +02:00
Guido Schmitz
a5a8cfa17d Fix adding multiple devices simultaneously to devolo Home Network's device tracker (#129082) 2024-10-25 20:43:02 +02:00
tronikos
60c3e701e9 Partially revert "LLM Tool parameters check (#123621)" (#129064) 2024-10-25 20:42:55 +02:00
Bram Kragten
b9b129dcf5 Update frontend to 20241002.4 (#129049) 2024-10-25 20:42:48 +02:00
Daniel Albers
d882ab236a Remove DHCP match from awair (#129047)
Co-authored-by: Joostlek <joostlek@outlook.com>
2024-10-25 20:42:40 +02:00
Joost Lekkerkerker
140cc0e486 Bump yt-dlp to 2024.10.22 (#129034) 2024-10-25 20:42:17 +02:00
Guido Schmitz
6ac7c0f893 Fix devolo_home_network devices not reporting a MAC address (#129021) 2024-10-25 20:42:11 +02:00
J. Nick Koston
096d50617f Fix cancellation leaking upward from the timeout util (#129003) 2024-10-25 20:42:04 +02:00
Simone Chemelli
9dd8c0cc4f Fix uptime floating values for Vodafone Station (#128974) 2024-10-25 20:41:57 +02:00
Maikel Punie
de0fab86ec Bump pyduotecno to 2024.10.1 (#128968) 2024-10-25 20:39:38 +02:00
Simone Chemelli
ada837ee95 Add diagnostics to Vodafone Station (#128923)
* Add diagnostics to Vodafone Station

* cleanup and exclude props based on date
2024-10-25 20:22:47 +02:00
Daniel Hjelseth Høyer
67e73173f6 Bump pyTibber to 0.30.3 (#128860) 2024-10-25 20:22:40 +02:00
Simone Chemelli
029411d3fa Add diagnostics to Comelit SimpleHome (#128794)
* Add diagnostics to Comelit SimpleHome

* add test

* add missing tests

* introduce SnapshotAssertion

* cleanup

* exclude date based props
2024-10-25 20:12:54 +02:00
Steven B.
6ba033f934 Bump ring-doorbell library to 0.9.8 (#128662) 2024-10-25 20:12:48 +02:00
Steven B.
336742e335 Bump ring-doorbell to 0.9.7 (#127554) 2024-10-25 20:12:41 +02:00
mkmer
4a94430bf0 Handle temprorary hold in Honeywell (#128460) 2024-10-25 20:05:14 +02:00
David Bonnes
cc337f7b1e Fix evohome regression preventing helpful messages when setup fails (#126441)
Co-authored-by: Robert Resch <robert@resch.dev>
2024-10-25 20:05:05 +02:00
Franck Nijhof
515771553f Bump version to 2024.10.3 2024-10-18 13:22:48 +02:00
mvn23
e204812d2b Bump pyotgw to 2.2.2 (#128594) 2024-10-18 13:22:22 +02:00
Petar Petrov
ca703cb858 Increase Z-Wave fallback thermostat range to 0-50 C (#128543)
* Z-Wave JS: Increase fallback thermostat range to 0-50 C

* update test
2024-10-18 13:21:49 +02:00
Louis Christ
b018d4a97d Bump pyblu to 1.0.4 (#128482) 2024-10-18 13:21:46 +02:00
dontinelli
146768ff8a Bump solarlog_cli to 0.3.2 (#128293) 2024-10-18 13:21:43 +02:00
Allen Porter
ea7473ed67 Bump gcal_sync to 6.1.6 (#128270) 2024-10-18 13:21:40 +02:00
Álvaro Fernández Rojas
0e8393766f Update aioairzone to v0.9.5 (#128265)
Signed-off-by: Álvaro Fernández Rojas <noltari@gmail.com>
2024-10-18 13:21:36 +02:00
Álvaro Fernández Rojas
7d2536c503 Update aioairzone to v0.9.4 (#127792) 2024-10-18 13:21:31 +02:00
Simon Lamon
f9cbf1b30c Keep the provided name when creating a tag (#128240)
* Keep the name

* Add patch

* Update homeassistant/components/tag/__init__.py

Co-authored-by: G Johansson <goran.johansson@shiftit.se>

---------

Co-authored-by: G Johansson <goran.johansson@shiftit.se>
2024-10-18 13:20:21 +02:00
Adam Petrovic
d66d87d271 Fix daikin entities not refreshing quickly (#128230)
* Fix daikin entities not refreshing quickly

* Update homeassistant/components/daikin/switch.py

---------

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2024-10-18 13:20:18 +02:00
Joost Lekkerkerker
5a8fa6cf38 Bump yt-dlp to 2024.10.07 (#128182) 2024-10-18 13:20:15 +02:00
Chris Talkington
76340035db Fix playing media via roku (#128133)
* re-support playing media via roku

* fixes

* test fixes

* Update test_media_player.py

* always send media type

* add description to options flow
2024-10-18 13:20:12 +02:00
puddly
0a26e68d0c Use the same ZHA database path during startup and when loading device triggers (#128130)
Use the same zigpy database path source as in the radio manager
2024-10-18 13:20:09 +02:00
Marc Hörsken
14127b910f Improve discovery of WMS WebControl pro by updating IP address (#128007) 2024-10-18 13:20:06 +02:00
Michael
ba4d081021 Fix printer uptime fluctuations in IPP (#127725)
* decrease uptime accuracy from seconds to minutes

* adjust tests

* calc uptime timestamp in coordinator

* bump pyipp to 0.17.0

* revert changes, just use the new printer.booted_at property

---------

Co-authored-by: Chris Talkington <chris@talkingtontech.com>
2024-10-18 13:19:43 +02:00
Marc Mueller
18d65d513e Update home-assistant-bluetooth to 1.13.0 (#127691) 2024-10-18 13:18:52 +02:00
68 changed files with 1512 additions and 127 deletions

View File

@@ -85,6 +85,7 @@ HVAC_MODE_LIB_TO_HASS: Final[dict[OperationMode, HVACMode]] = {
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 @@ class AirzoneClimate(AirzoneZoneEntity, ClimateEntity):
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

View File

@@ -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"]
}

View File

@@ -3,11 +3,6 @@
"name": "Awair",
"codeowners": ["@ahayworth", "@danielsjf"],
"config_flow": true,
"dhcp": [
{
"macaddress": "70886B1*"
}
],
"documentation": "https://www.home-assistant.io/integrations/awair",
"iot_class": "local_polling",
"loggers": ["python_awair"],

View File

@@ -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."

View File

@@ -493,6 +493,8 @@ class BluesoundPlayer(MediaPlayerEntity):
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()

View File

@@ -0,0 +1,93 @@
"""Diagnostics support for Comelit integration."""
from __future__ import annotations
from typing import Any
from aiocomelit import (
ComelitSerialBridgeObject,
ComelitVedoAreaObject,
ComelitVedoZoneObject,
)
from aiocomelit.const import BRIDGE
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PIN, CONF_TYPE
from homeassistant.core import HomeAssistant
from .const import DOMAIN
from .coordinator import ComelitBaseCoordinator
TO_REDACT = {CONF_PIN}
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: ConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
coordinator: ComelitBaseCoordinator = hass.data[DOMAIN][entry.entry_id]
dev_list: list[dict[str, Any]] = []
dev_type_list: list[dict[int, Any]] = []
for dev_type in coordinator.data:
dev_type_list = []
for sensor_data in coordinator.data[dev_type].values():
if isinstance(sensor_data, ComelitSerialBridgeObject):
dev_type_list.append(
{
sensor_data.index: {
"name": sensor_data.name,
"status": sensor_data.status,
"human_status": sensor_data.human_status,
"protected": sensor_data.protected,
"val": sensor_data.val,
"zone": sensor_data.zone,
"power": sensor_data.power,
"power_unit": sensor_data.power_unit,
}
}
)
if isinstance(sensor_data, ComelitVedoAreaObject):
dev_type_list.append(
{
sensor_data.index: {
"name": sensor_data.name,
"human_status": sensor_data.human_status.value,
"p1": sensor_data.p1,
"p2": sensor_data.p2,
"ready": sensor_data.ready,
"armed": sensor_data.armed,
"alarm": sensor_data.alarm,
"alarm_memory": sensor_data.alarm_memory,
"sabotage": sensor_data.sabotage,
"anomaly": sensor_data.anomaly,
"in_time": sensor_data.in_time,
"out_time": sensor_data.out_time,
}
}
)
if isinstance(sensor_data, ComelitVedoZoneObject):
dev_type_list.append(
{
sensor_data.index: {
"name": sensor_data.name,
"human_status": sensor_data.human_status.value,
"status": sensor_data.status,
"status_api": sensor_data.status_api,
}
}
)
dev_list.append({dev_type: dev_type_list})
return {
"entry": async_redact_data(entry.as_dict(), TO_REDACT),
"type": entry.data.get(CONF_TYPE, BRIDGE),
"device_info": {
"last_update success": coordinator.last_update_success,
"last_exception": repr(coordinator.last_exception),
"devices": dev_list,
},
}

View File

@@ -159,6 +159,7 @@ class DaikinClimate(DaikinEntity, ClimateEntity):
if values:
await self.device.set(values)
await self.coordinator.async_refresh()
@property
def unique_id(self) -> str:
@@ -261,6 +262,7 @@ class DaikinClimate(DaikinEntity, ClimateEntity):
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 @@ class DaikinClimate(DaikinEntity, ClimateEntity):
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()

View File

@@ -63,10 +63,12 @@ class DaikinZoneSwitch(DaikinEntity, SwitchEntity):
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 @@ class DaikinStreamerSwitch(DaikinEntity, SwitchEntity):
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 @@ class DaikinToggleSwitch(DaikinEntity, SwitchEntity):
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()

View File

@@ -51,7 +51,7 @@ async def async_setup_entry(
)
)
tracked.add(station.mac_address)
async_add_entities(new_entities)
async_add_entities(new_entities)
@callback
def restore_entities() -> None:

View File

@@ -9,6 +9,7 @@ from devolo_plc_api.device_api import (
)
from devolo_plc_api.plcnet_api import DataRate, LogicalNetwork
from homeassistant.const import ATTR_CONNECTIONS
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.update_coordinator import (
@@ -45,7 +46,6 @@ class DevoloEntity(Entity):
self._attr_device_info = DeviceInfo(
configuration_url=f"http://{self.device.ip}",
connections={(CONNECTION_NETWORK_MAC, self.device.mac)},
identifiers={(DOMAIN, str(self.device.serial_number))},
manufacturer="devolo",
model=self.device.product,
@@ -53,6 +53,10 @@ class DevoloEntity(Entity):
serial_number=self.device.serial_number,
sw_version=self.device.firmware_version,
)
if self.device.mac:
self._attr_device_info[ATTR_CONNECTIONS] = {
(CONNECTION_NETWORK_MAC, self.device.mac)
}
self._attr_translation_key = self.entity_description.key
self._attr_unique_id = (
f"{self.device.serial_number}_{self.entity_description.key}"

View File

@@ -7,5 +7,5 @@
"iot_class": "local_push",
"loggers": ["pyduotecno", "pyduotecno-node", "pyduotecno-unit"],
"quality_scale": "silver",
"requirements": ["pyDuotecno==2024.10.0"]
"requirements": ["pyDuotecno==2024.10.1"]
}

View File

@@ -223,7 +223,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
config[DOMAIN][CONF_PASSWORD],
)
except evo.AuthenticationFailed as err:
except (evo.AuthenticationFailed, evo.RequestFailed) as err:
handle_evo_exception(err)
return False

View File

@@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/frontend",
"integration_type": "system",
"quality_scale": "internal",
"requirements": ["home-assistant-frontend==20241002.3"]
"requirements": ["home-assistant-frontend==20241002.4"]
}

View File

@@ -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"]
}

View File

@@ -49,6 +49,10 @@ from .const import (
RETRY,
)
MODE_PERMANENT_HOLD = 2
MODE_TEMPORARY_HOLD = 1
MODE_HOLD = {MODE_PERMANENT_HOLD, MODE_TEMPORARY_HOLD}
ATTR_FAN_ACTION = "fan_action"
ATTR_PERMANENT_HOLD = "permanent_hold"
@@ -175,6 +179,7 @@ class HoneywellUSThermostat(ClimateEntity):
self._cool_away_temp = cool_away_temp
self._heat_away_temp = heat_away_temp
self._away = False
self._away_hold = False
self._retry = 0
self._attr_unique_id = str(device.deviceid)
@@ -323,11 +328,15 @@ class HoneywellUSThermostat(ClimateEntity):
@property
def preset_mode(self) -> str | None:
"""Return the current preset mode, e.g., home, away, temp."""
if self._away:
if self._away and self._is_hold():
self._away_hold = True
return PRESET_AWAY
if self._is_permanent_hold():
if self._is_hold():
return PRESET_HOLD
# Someone has changed the stat manually out of hold in away mode
if self._away and self._away_hold:
self._away = False
self._away_hold = False
return PRESET_NONE
@property
@@ -335,10 +344,15 @@ class HoneywellUSThermostat(ClimateEntity):
"""Return the fan setting."""
return HW_FAN_MODE_TO_HA.get(self._device.fan_mode)
def _is_hold(self) -> bool:
heat_status = self._device.raw_ui_data.get("StatusHeat", 0)
cool_status = self._device.raw_ui_data.get("StatusCool", 0)
return heat_status in MODE_HOLD or cool_status in MODE_HOLD
def _is_permanent_hold(self) -> bool:
heat_status = self._device.raw_ui_data.get("StatusHeat", 0)
cool_status = self._device.raw_ui_data.get("StatusCool", 0)
return heat_status == 2 or cool_status == 2
return MODE_PERMANENT_HOLD in (heat_status, cool_status)
async def _set_temperature(self, **kwargs) -> None:
"""Set new target temperature."""

View File

@@ -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."]
}

View File

@@ -4,7 +4,7 @@ from __future__ import annotations
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.const import ATTR_LOCATION, PERCENTAGE, EntityCategory
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 @@ PRINTER_SENSORS: tuple[IPPSensorEntityDescription, ...] = (
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,
),
)

View File

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

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/nyt_games",
"integration_type": "service",
"iot_class": "cloud_polling",
"requirements": ["nyt_games==0.4.3"]
"requirements": ["nyt_games==0.4.4"]
}

View File

@@ -139,7 +139,7 @@ CONNECTIONS_SENSORS: tuple[NYTGamesConnectionsSensorEntityDescription, ...] = (
state_class=SensorStateClass.TOTAL_INCREASING,
native_unit_of_measurement=UnitOfTime.DAYS,
device_class=SensorDeviceClass.DURATION,
value_fn=lambda connections: connections.current_streak,
value_fn=lambda connections: connections.max_streak,
),
)

View File

@@ -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"]
}

View File

@@ -14,5 +14,5 @@
"iot_class": "cloud_polling",
"loggers": ["ring_doorbell"],
"quality_scale": "silver",
"requirements": ["ring-doorbell==0.9.6"]
"requirements": ["ring-doorbell==0.9.8"]
}

View File

@@ -6,7 +6,7 @@ from homeassistant.config_entries import ConfigEntry
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)

View File

@@ -10,12 +10,17 @@ from rokuecp import Roku, RokuError
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 @@ class RokuConfigFlow(ConfigFlow, domain=DOMAIN):
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,
}
),
)

View File

@@ -15,3 +15,9 @@ DEFAULT_PORT = 8060
# Services
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
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

View File

@@ -445,17 +445,25 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity):
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

View File

@@ -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": {

View File

@@ -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"]
}

View File

@@ -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]):

View File

@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["tibber"],
"quality_scale": "silver",
"requirements": ["pyTibber==0.30.2"]
"requirements": ["pyTibber==0.30.3"]
}

View File

@@ -0,0 +1,47 @@
"""Diagnostics support for Vodafone Station."""
from __future__ import annotations
from typing import Any
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from .const import DOMAIN
from .coordinator import VodafoneStationRouter
TO_REDACT = {CONF_USERNAME, CONF_PASSWORD}
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: ConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
coordinator: VodafoneStationRouter = hass.data[DOMAIN][entry.entry_id]
sensors_data = coordinator.data.sensors
return {
"entry": async_redact_data(entry.as_dict(), TO_REDACT),
"device_info": {
"sys_model_name": sensors_data.get("sys_model_name"),
"sys_firmware_version": sensors_data["sys_firmware_version"],
"sys_hardware_version": sensors_data["sys_hardware_version"],
"sys_cpu_usage": sensors_data["sys_cpu_usage"][:-1],
"sys_memory_usage": sensors_data["sys_memory_usage"][:-1],
"sys_reboot_cause": sensors_data["sys_reboot_cause"],
"last_update success": coordinator.last_update_success,
"last_exception": coordinator.last_exception,
"client_devices": [
{
"hostname": device_info.device.name,
"connection_type": device_info.device.connection_type,
"connected": device_info.device.connected,
"type": device_info.device.type,
}
for _, device_info in coordinator.data.devices.items()
],
},
}

View File

@@ -23,25 +23,42 @@ from .const import _LOGGER, DOMAIN, LINE_TYPES
from .coordinator import VodafoneStationRouter
NOT_AVAILABLE: list = ["", "N/A", "0.0.0.0"]
UPTIME_DEVIATION = 30
@dataclass(frozen=True, kw_only=True)
class VodafoneStationEntityDescription(SensorEntityDescription):
"""Vodafone Station entity description."""
value: Callable[[Any, Any], Any] = (
lambda coordinator, key: coordinator.data.sensors[key]
value: Callable[[Any, Any, Any], Any] = (
lambda coordinator, last_value, key: coordinator.data.sensors[key]
)
is_suitable: Callable[[dict], bool] = lambda val: True
def _calculate_uptime(coordinator: VodafoneStationRouter, key: str) -> datetime:
def _calculate_uptime(
coordinator: VodafoneStationRouter,
last_value: datetime | None,
key: str,
) -> datetime:
"""Calculate device uptime."""
return coordinator.api.convert_uptime(coordinator.data.sensors[key])
delta_uptime = coordinator.api.convert_uptime(coordinator.data.sensors[key])
if (
not last_value
or abs((delta_uptime - last_value).total_seconds()) > UPTIME_DEVIATION
):
return delta_uptime
return last_value
def _line_connection(coordinator: VodafoneStationRouter, key: str) -> str | None:
def _line_connection(
coordinator: VodafoneStationRouter,
last_value: str | None,
key: str,
) -> str | None:
"""Identify line type."""
value = coordinator.data.sensors
@@ -126,14 +143,18 @@ SENSOR_TYPES: Final = (
translation_key="sys_cpu_usage",
native_unit_of_measurement=PERCENTAGE,
entity_category=EntityCategory.DIAGNOSTIC,
value=lambda coordinator, key: float(coordinator.data.sensors[key][:-1]),
value=lambda coordinator, last_value, key: float(
coordinator.data.sensors[key][:-1]
),
),
VodafoneStationEntityDescription(
key="sys_memory_usage",
translation_key="sys_memory_usage",
native_unit_of_measurement=PERCENTAGE,
entity_category=EntityCategory.DIAGNOSTIC,
value=lambda coordinator, key: float(coordinator.data.sensors[key][:-1]),
value=lambda coordinator, last_value, key: float(
coordinator.data.sensors[key][:-1]
),
),
VodafoneStationEntityDescription(
key="sys_reboot_cause",
@@ -178,10 +199,12 @@ class VodafoneStationSensorEntity(
self.entity_description = description
self._attr_device_info = coordinator.device_info
self._attr_unique_id = f"{coordinator.serial_number}_{description.key}"
self._old_state = None
@property
def native_value(self) -> StateType:
"""Sensor value."""
return self.entity_description.value(
self.coordinator, self.entity_description.key
self._old_state = self.entity_description.value(
self.coordinator, self._old_state, self.entity_description.key
)
return self._old_state

View File

@@ -2,6 +2,7 @@
from __future__ import annotations
import ipaddress
import logging
from typing import Any
@@ -38,7 +39,19 @@ class WebControlProConfigFlow(ConfigFlow, domain=DOMAIN):
"""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 (

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
# 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),
)

View File

@@ -24,8 +24,6 @@ from homeassistant.components.climate import (
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 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity):
@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 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity):
@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])

View File

@@ -24,7 +24,7 @@ if TYPE_CHECKING:
APPLICATION_NAME: Final = "HomeAssistant"
MAJOR_VERSION: Final = 2024
MINOR_VERSION: Final = 10
PATCH_VERSION: Final = "2"
PATCH_VERSION: Final = "4"
__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)

View File

@@ -37,10 +37,6 @@ DHCP: Final[list[dict[str, str | bool]]] = [
"hostname": "august*",
"macaddress": "E076D0*",
},
{
"domain": "awair",
"macaddress": "70886B1*",
},
{
"domain": "axis",
"registered_devices": True,

View File

@@ -177,11 +177,6 @@ class APIInstance:
else:
raise HomeAssistantError(f'Tool "{tool_input.tool_name}" not found')
tool_input = ToolInput(
tool_name=tool_input.tool_name,
tool_args=tool.parameters(tool_input.tool_args),
)
return await tool.async_call(self.api.hass, tool_input, self.llm_context)

View File

@@ -31,8 +31,8 @@ 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-frontend==20241002.3
home-assistant-bluetooth==1.13.0
home-assistant-frontend==20241002.4
home-assistant-intents==2024.10.2
httpx==0.27.2
ifaddr==0.2.0

View File

@@ -16,7 +16,7 @@ from .async_ import run_callback_threadsafe
ZONE_GLOBAL = "global"
class _State(str, enum.Enum):
class _State(enum.Enum):
"""States of a task."""
INIT = "INIT"
@@ -160,11 +160,16 @@ class _GlobalTaskContext:
self._wait_zone: asyncio.Event = asyncio.Event()
self._state: _State = _State.INIT
self._cool_down: float = cool_down
self._cancelling = 0
async def __aenter__(self) -> Self:
self._manager.global_tasks.append(self)
self._start_timer()
self._state = _State.ACTIVE
# Remember if the task was already cancelling
# so when we __aexit__ we can decide if we should
# raise asyncio.TimeoutError or let the cancellation propagate
self._cancelling = self._task.cancelling()
return self
async def __aexit__(
@@ -177,7 +182,15 @@ class _GlobalTaskContext:
self._manager.global_tasks.remove(self)
# Timeout on exit
if exc_type is asyncio.CancelledError and self.state == _State.TIMEOUT:
if exc_type is asyncio.CancelledError and self.state is _State.TIMEOUT:
# The timeout was hit, and the task was cancelled
# so we need to uncancel the task since the cancellation
# should not leak out of the context manager
if self._task.uncancel() > self._cancelling:
# If the task was already cancelling don't raise
# asyncio.TimeoutError and instead return None
# to allow the cancellation to propagate
return None
raise TimeoutError
self._state = _State.EXIT
@@ -266,6 +279,7 @@ class _ZoneTaskContext:
self._time_left: float = timeout
self._expiration_time: float | None = None
self._timeout_handler: asyncio.Handle | None = None
self._cancelling = 0
@property
def state(self) -> _State:
@@ -280,6 +294,11 @@ class _ZoneTaskContext:
if self._zone.freezes_done:
self._start_timer()
# Remember if the task was already cancelling
# so when we __aexit__ we can decide if we should
# raise asyncio.TimeoutError or let the cancellation propagate
self._cancelling = self._task.cancelling()
return self
async def __aexit__(
@@ -292,7 +311,15 @@ class _ZoneTaskContext:
self._stop_timer()
# Timeout on exit
if exc_type is asyncio.CancelledError and self.state == _State.TIMEOUT:
if exc_type is asyncio.CancelledError and self.state is _State.TIMEOUT:
# The timeout was hit, and the task was cancelled
# so we need to uncancel the task since the cancellation
# should not leak out of the context manager
if self._task.uncancel() > self._cancelling:
# If the task was already cancelling don't raise
# asyncio.TimeoutError and instead return None
# to allow the cancellation to propagate
return None
raise TimeoutError
self._state = _State.EXIT

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "homeassistant"
version = "2024.10.2"
version = "2024.10.4"
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",

View File

@@ -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

View File

@@ -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
@@ -1117,7 +1117,7 @@ hole==0.8.0
holidays==0.58
# homeassistant.components.frontend
home-assistant-frontend==20241002.3
home-assistant-frontend==20241002.4
# homeassistant.components.conversation
home-assistant-intents==2024.10.2
@@ -1484,7 +1484,7 @@ numato-gpio==0.13.0
numpy==1.26.4
# homeassistant.components.nyt_games
nyt_games==0.4.3
nyt_games==0.4.4
# homeassistant.components.oasa_telematics
oasatelematics==0.3
@@ -1710,7 +1710,7 @@ pyCEC==0.5.2
pyControl4==1.2.0
# homeassistant.components.duotecno
pyDuotecno==2024.10.0
pyDuotecno==2024.10.1
# homeassistant.components.electrasmart
pyElectra==1.2.4
@@ -1728,7 +1728,7 @@ pyRFXtrx==0.31.1
pySDCP==1
# homeassistant.components.tibber
pyTibber==0.30.2
pyTibber==0.30.3
# homeassistant.components.dlink
pyW215==0.7.0
@@ -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
@@ -2543,7 +2543,7 @@ rfk101py==0.0.1
rflink==0.0.66
# homeassistant.components.ring
ring-doorbell==0.9.6
ring-doorbell==0.9.8
# homeassistant.components.fleetgo
ritassist==0.9.2
@@ -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.22
# homeassistant.components.zamg
zamg==0.3.6

View File

@@ -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
@@ -943,7 +943,7 @@ hole==0.8.0
holidays==0.58
# homeassistant.components.frontend
home-assistant-frontend==20241002.3
home-assistant-frontend==20241002.4
# homeassistant.components.conversation
home-assistant-intents==2024.10.2
@@ -1232,7 +1232,7 @@ numato-gpio==0.13.0
numpy==1.26.4
# homeassistant.components.nyt_games
nyt_games==0.4.3
nyt_games==0.4.4
# homeassistant.components.google
oauth2client==4.1.3
@@ -1396,7 +1396,7 @@ pyCEC==0.5.2
pyControl4==1.2.0
# homeassistant.components.duotecno
pyDuotecno==2024.10.0
pyDuotecno==2024.10.1
# homeassistant.components.electrasmart
pyElectra==1.2.4
@@ -1405,7 +1405,7 @@ pyElectra==1.2.4
pyRFXtrx==0.31.1
# homeassistant.components.tibber
pyTibber==0.30.2
pyTibber==0.30.3
# homeassistant.components.dlink
pyW215==0.7.0
@@ -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
@@ -2025,7 +2025,7 @@ reolink-aio==0.9.11
rflink==0.0.66
# homeassistant.components.ring
ring-doorbell==0.9.6
ring-doorbell==0.9.8
# homeassistant.components.roku
rokuecp==0.19.3
@@ -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.22
# homeassistant.components.zamg
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({
@@ -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,
}),
}),
}),
})

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_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

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

@@ -1,6 +1,19 @@
"""Common stuff for Comelit SimpleHome tests."""
from aiocomelit.const import VEDO
from aiocomelit import ComelitVedoAreaObject, ComelitVedoZoneObject
from aiocomelit.api import ComelitSerialBridgeObject
from aiocomelit.const import (
CLIMATE,
COVER,
IRRIGATION,
LIGHT,
OTHER,
SCENARIO,
VEDO,
WATT,
AlarmAreaState,
AlarmZoneState,
)
from homeassistant.components.comelit.const import DOMAIN
from homeassistant.const import CONF_DEVICES, CONF_HOST, CONF_PIN, CONF_PORT, CONF_TYPE
@@ -27,3 +40,67 @@ MOCK_USER_BRIDGE_DATA = MOCK_CONFIG[DOMAIN][CONF_DEVICES][0]
MOCK_USER_VEDO_DATA = MOCK_CONFIG[DOMAIN][CONF_DEVICES][1]
FAKE_PIN = 5678
BRIDGE_DEVICE_QUERY = {
CLIMATE: {},
COVER: {
0: ComelitSerialBridgeObject(
index=0,
name="Cover0",
status=0,
human_status="closed",
type="cover",
val=0,
protected=0,
zone="Open space",
power=0.0,
power_unit=WATT,
)
},
LIGHT: {
0: ComelitSerialBridgeObject(
index=0,
name="Light0",
status=0,
human_status="off",
type="light",
val=0,
protected=0,
zone="Bathroom",
power=0.0,
power_unit=WATT,
)
},
OTHER: {},
IRRIGATION: {},
SCENARIO: {},
}
VEDO_DEVICE_QUERY = {
"aree": {
0: ComelitVedoAreaObject(
index=0,
name="Area0",
p1=True,
p2=False,
ready=False,
armed=False,
alarm=False,
alarm_memory=False,
sabotage=False,
anomaly=False,
in_time=False,
out_time=False,
human_status=AlarmAreaState.UNKNOWN,
)
},
"zone": {
0: ComelitVedoZoneObject(
index=0,
name="Zone0",
status_api="0x000",
status=0,
human_status=AlarmZoneState.REST,
)
},
}

View File

@@ -0,0 +1,144 @@
# serializer version: 1
# name: test_entry_diagnostics_bridge
dict({
'device_info': dict({
'devices': list([
dict({
'clima': list([
]),
}),
dict({
'shutter': list([
dict({
'0': dict({
'human_status': 'closed',
'name': 'Cover0',
'power': 0.0,
'power_unit': 'W',
'protected': 0,
'status': 0,
'val': 0,
'zone': 'Open space',
}),
}),
]),
}),
dict({
'light': list([
dict({
'0': dict({
'human_status': 'off',
'name': 'Light0',
'power': 0.0,
'power_unit': 'W',
'protected': 0,
'status': 0,
'val': 0,
'zone': 'Bathroom',
}),
}),
]),
}),
dict({
'other': list([
]),
}),
dict({
'irrigation': list([
]),
}),
dict({
'scenario': list([
]),
}),
]),
'last_exception': 'None',
'last_update success': True,
}),
'entry': dict({
'data': dict({
'host': 'fake_host',
'pin': '**REDACTED**',
'port': 80,
}),
'disabled_by': None,
'discovery_keys': dict({
}),
'domain': 'comelit',
'minor_version': 1,
'options': dict({
}),
'pref_disable_new_entities': False,
'pref_disable_polling': False,
'source': 'user',
'title': 'Mock Title',
'unique_id': None,
'version': 1,
}),
'type': 'Serial bridge',
})
# ---
# name: test_entry_diagnostics_vedo
dict({
'device_info': dict({
'devices': list([
dict({
'aree': list([
dict({
'0': dict({
'alarm': False,
'alarm_memory': False,
'anomaly': False,
'armed': False,
'human_status': 'unknown',
'in_time': False,
'name': 'Area0',
'out_time': False,
'p1': True,
'p2': False,
'ready': False,
'sabotage': False,
}),
}),
]),
}),
dict({
'zone': list([
dict({
'0': dict({
'human_status': 'rest',
'name': 'Zone0',
'status': 0,
'status_api': '0x000',
}),
}),
]),
}),
]),
'last_exception': 'None',
'last_update success': True,
}),
'entry': dict({
'data': dict({
'host': 'fake_vedo_host',
'pin': '**REDACTED**',
'port': 8080,
'type': 'Vedo system',
}),
'disabled_by': None,
'discovery_keys': dict({
}),
'domain': 'comelit',
'minor_version': 1,
'options': dict({
}),
'pref_disable_new_entities': False,
'pref_disable_polling': False,
'source': 'user',
'title': 'Mock Title',
'unique_id': None,
'version': 1,
}),
'type': 'Vedo system',
})
# ---

View File

@@ -0,0 +1,81 @@
"""Tests for Comelit Simplehome diagnostics platform."""
from __future__ import annotations
from unittest.mock import patch
from syrupy import SnapshotAssertion
from syrupy.filters import props
from homeassistant.components.comelit.const import DOMAIN
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant
from .const import (
BRIDGE_DEVICE_QUERY,
MOCK_USER_BRIDGE_DATA,
MOCK_USER_VEDO_DATA,
VEDO_DEVICE_QUERY,
)
from tests.common import MockConfigEntry
from tests.components.diagnostics import get_diagnostics_for_config_entry
from tests.typing import ClientSessionGenerator
async def test_entry_diagnostics_bridge(
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
snapshot: SnapshotAssertion,
) -> None:
"""Test Bridge config entry diagnostics."""
entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_BRIDGE_DATA)
entry.add_to_hass(hass)
with (
patch("aiocomelit.api.ComeliteSerialBridgeApi.login"),
patch(
"aiocomelit.api.ComeliteSerialBridgeApi.get_all_devices",
return_value=BRIDGE_DEVICE_QUERY,
),
):
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
assert entry.state == ConfigEntryState.LOADED
assert await get_diagnostics_for_config_entry(hass, hass_client, entry) == snapshot(
exclude=props(
"entry_id",
"created_at",
"modified_at",
)
)
async def test_entry_diagnostics_vedo(
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
snapshot: SnapshotAssertion,
) -> None:
"""Test Vedo System config entry diagnostics."""
entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_VEDO_DATA)
entry.add_to_hass(hass)
with (
patch("aiocomelit.api.ComelitVedoApi.login"),
patch(
"aiocomelit.api.ComelitVedoApi.get_all_areas_and_zones",
return_value=VEDO_DEVICE_QUERY,
),
):
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
assert entry.state == ConfigEntryState.LOADED
assert await get_diagnostics_for_config_entry(hass, hass_client, entry) == snapshot(
exclude=props(
"entry_id",
"created_at",
"modified_at",
)
)

View File

@@ -50,7 +50,7 @@ class MockDevice(Device):
self, session_instance: httpx.AsyncClient | None = None
) -> None:
"""Give a mocked device the needed properties."""
self.mac = DISCOVERY_INFO.properties["PlcMacAddress"]
self.mac = DISCOVERY_INFO.properties["PlcMacAddress"] if self.plcnet else None
self.mt_number = DISCOVERY_INFO.properties["MT"]
self.product = DISCOVERY_INFO.properties["Product"]
self.serial_number = DISCOVERY_INFO.properties["SN"]

View File

@@ -1,5 +1,5 @@
# serializer version: 1
# name: test_setup_entry
# name: test_setup_entry[mock_device]
DeviceRegistryEntrySnapshot({
'area_id': None,
'config_entries': <ANY>,
@@ -35,3 +35,35 @@
'via_device_id': None,
})
# ---
# name: test_setup_entry[mock_repeater_device]
DeviceRegistryEntrySnapshot({
'area_id': None,
'config_entries': <ANY>,
'configuration_url': 'http://192.0.2.1',
'connections': set({
}),
'disabled_by': None,
'entry_type': None,
'hw_version': None,
'id': <ANY>,
'identifiers': set({
tuple(
'devolo_home_network',
'1234567890',
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'devolo',
'model': 'dLAN pro 1200+ WiFi ac',
'model_id': '2730',
'name': 'Mock Title',
'name_by_user': None,
'primary_config_entry': <ANY>,
'serial_number': '1234567890',
'suggested_area': None,
'sw_version': '5.6.1',
'via_device_id': None,
})
# ---

View File

@@ -27,13 +27,16 @@ from .mock import MockDevice
from tests.common import MockConfigEntry
@pytest.mark.parametrize("device", ["mock_device", "mock_repeater_device"])
async def test_setup_entry(
hass: HomeAssistant,
mock_device: MockDevice,
device: str,
device_registry: dr.DeviceRegistry,
snapshot: SnapshotAssertion,
request: pytest.FixtureRequest,
) -> None:
"""Test setup entry."""
mock_device: MockDevice = request.getfixturevalue(device)
entry = configure_integration(hass)
assert await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()

View File

@@ -2,11 +2,19 @@
from __future__ import annotations
from http import HTTPStatus
import logging
from unittest.mock import patch
from evohomeasync2 import exceptions as exc
from evohomeasync2.broker import _ERR_MSG_LOOKUP_AUTH, _ERR_MSG_LOOKUP_BASE
from freezegun.api import FrozenDateTimeFactory
import pytest
from syrupy import SnapshotAssertion
from homeassistant.components.evohome import DOMAIN
from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
from .conftest import setup_evohome
from .const import TEST_INSTALLS
@@ -28,3 +36,112 @@ async def test_entities(
await setup_evohome(hass, config, install=install)
assert hass.states.async_all() == snapshot
SETUP_FAILED_ANTICIPATED = (
"homeassistant.setup",
logging.ERROR,
"Setup failed for 'evohome': Integration failed to initialize.",
)
SETUP_FAILED_UNEXPECTED = (
"homeassistant.setup",
logging.ERROR,
"Error during setup of component evohome",
)
AUTHENTICATION_FAILED = (
"homeassistant.components.evohome.helpers",
logging.ERROR,
"Failed to authenticate with the vendor's server. Check your username"
" and password. NB: Some special password characters that work"
" correctly via the website will not work via the web API. Message"
" is: ",
)
REQUEST_FAILED_NONE = (
"homeassistant.components.evohome.helpers",
logging.WARNING,
"Unable to connect with the vendor's server. "
"Check your network and the vendor's service status page. "
"Message is: ",
)
REQUEST_FAILED_503 = (
"homeassistant.components.evohome.helpers",
logging.WARNING,
"The vendor says their server is currently unavailable. "
"Check the vendor's service status page",
)
REQUEST_FAILED_429 = (
"homeassistant.components.evohome.helpers",
logging.WARNING,
"The vendor's API rate limit has been exceeded. "
"If this message persists, consider increasing the scan_interval",
)
REQUEST_FAILED_LOOKUP = {
None: [
REQUEST_FAILED_NONE,
SETUP_FAILED_ANTICIPATED,
],
HTTPStatus.SERVICE_UNAVAILABLE: [
REQUEST_FAILED_503,
SETUP_FAILED_ANTICIPATED,
],
HTTPStatus.TOO_MANY_REQUESTS: [
REQUEST_FAILED_429,
SETUP_FAILED_ANTICIPATED,
],
}
@pytest.mark.parametrize(
"status", [*sorted([*_ERR_MSG_LOOKUP_AUTH, HTTPStatus.BAD_GATEWAY]), None]
)
async def test_authentication_failure_v2(
hass: HomeAssistant,
config: dict[str, str],
status: HTTPStatus,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test failure to setup an evohome-compatible system.
In this instance, the failure occurs in the v2 API.
"""
with patch("evohomeasync2.broker.Broker.get") as mock_fcn:
mock_fcn.side_effect = exc.AuthenticationFailed("", status=status)
with caplog.at_level(logging.WARNING):
result = await async_setup_component(hass, DOMAIN, {DOMAIN: config})
assert result is False
assert caplog.record_tuples == [
AUTHENTICATION_FAILED,
SETUP_FAILED_ANTICIPATED,
]
@pytest.mark.parametrize(
"status", [*sorted([*_ERR_MSG_LOOKUP_BASE, HTTPStatus.BAD_GATEWAY]), None]
)
async def test_client_request_failure_v2(
hass: HomeAssistant,
config: dict[str, str],
status: HTTPStatus,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test failure to setup an evohome-compatible system.
In this instance, the failure occurs in the v2 API.
"""
with patch("evohomeasync2.broker.Broker.get") as mock_fcn:
mock_fcn.side_effect = exc.RequestFailed("", status=status)
with caplog.at_level(logging.WARNING):
result = await async_setup_component(hass, DOMAIN, {DOMAIN: config})
assert result is False
assert caplog.record_tuples == REQUEST_FAILED_LOOKUP.get(
status, [SETUP_FAILED_UNEXPECTED]
)

View File

@@ -5,6 +5,7 @@ from unittest.mock import MagicMock
from aiohttp import ClientConnectionError
import aiosomecomfort
from freezegun.api import FrozenDateTimeFactory
import pytest
from syrupy.assertion import SnapshotAssertion
from syrupy.filters import props
@@ -29,6 +30,8 @@ from homeassistant.components.climate import (
)
from homeassistant.components.honeywell.climate import (
DOMAIN,
MODE_PERMANENT_HOLD,
MODE_TEMPORARY_HOLD,
PRESET_HOLD,
RETRY,
SCAN_INTERVAL,
@@ -1207,3 +1210,59 @@ async def test_unique_id(
await init_integration(hass, config_entry)
entity_entry = entity_registry.async_get(f"climate.{device.name}")
assert entity_entry.unique_id == str(device.deviceid)
async def test_preset_mode(
hass: HomeAssistant,
device: MagicMock,
config_entry: er.EntityRegistry,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test mode settings properly reflected."""
await init_integration(hass, config_entry)
entity_id = f"climate.{device.name}"
device.raw_ui_data["StatusHeat"] = 3
device.raw_ui_data["StatusCool"] = 3
freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done()
state = hass.states.get(entity_id)
assert state.attributes[ATTR_PRESET_MODE] == PRESET_NONE
device.raw_ui_data["StatusHeat"] = MODE_TEMPORARY_HOLD
device.raw_ui_data["StatusCool"] = MODE_TEMPORARY_HOLD
freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done()
state = hass.states.get(entity_id)
assert state.attributes[ATTR_PRESET_MODE] == PRESET_HOLD
device.raw_ui_data["StatusHeat"] = MODE_PERMANENT_HOLD
device.raw_ui_data["StatusCool"] = MODE_PERMANENT_HOLD
freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done()
state = hass.states.get(entity_id)
assert state.attributes[ATTR_PRESET_MODE] == PRESET_HOLD
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_PRESET_MODE,
{ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_AWAY},
blocking=True,
)
state = hass.states.get(entity_id)
assert state.attributes[ATTR_PRESET_MODE] == PRESET_AWAY
device.raw_ui_data["StatusHeat"] = 3
device.raw_ui_data["StatusCool"] = 3
freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done()
state = hass.states.get(entity_id)
assert state.attributes[ATTR_PRESET_MODE] == PRESET_NONE

View File

@@ -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,

View File

@@ -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.components.diagnostics import get_diagnostics_for_config_entry
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,

View File

@@ -98,7 +98,7 @@
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '0',
'state': '2',
})
# ---
# name: test_all_entities[sensor.connections_last_played-entry]

View File

@@ -6,7 +6,7 @@ from unittest.mock import MagicMock
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",
}

View File

@@ -32,6 +32,7 @@ from homeassistant.components.roku.const import (
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)

View File

@@ -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

View File

@@ -1,5 +1,7 @@
"""Common stuff for Vodafone Station tests."""
from aiovodafone.api import VodafoneStationDevice
from homeassistant.components.vodafone_station.const import DOMAIN
from homeassistant.const import CONF_DEVICES, CONF_HOST, CONF_PASSWORD, CONF_USERNAME
@@ -16,3 +18,98 @@ MOCK_CONFIG = {
}
MOCK_USER_DATA = MOCK_CONFIG[DOMAIN][CONF_DEVICES][0]
DEVICE_DATA_QUERY = {
"xx:xx:xx:xx:xx:xx": VodafoneStationDevice(
connected=True,
connection_type="wifi",
ip_address="192.168.1.10",
name="WifiDevice0",
mac="xx:xx:xx:xx:xx:xx",
type="laptop",
wifi="2.4G",
)
}
SENSOR_DATA_QUERY = {
"sys_serial_number": "M123456789",
"sys_firmware_version": "XF6_4.0.05.04",
"sys_bootloader_version": "0220",
"sys_hardware_version": "RHG3006 v1",
"omci_software_version": "\t\t1.0.0.1_41032\t\t\n",
"sys_uptime": "12:16:41",
"sys_cpu_usage": "97%",
"sys_reboot_cause": "Web Reboot",
"sys_memory_usage": "51.94%",
"sys_wireless_driver_version": "17.10.188.75;17.10.188.75",
"sys_wireless_driver_version_5g": "17.10.188.75;17.10.188.75",
"vf_internet_key_online_since": "",
"vf_internet_key_ip_addr": "0.0.0.0",
"vf_internet_key_system": "0.0.0.0",
"vf_internet_key_mode": "Auto",
"sys_voip_version": "v02.01.00_01.13a\n",
"sys_date_time": "20.10.2024 | 03:44 pm",
"sys_build_time": "Sun Jun 23 17:55:49 CST 2024\n",
"sys_model_name": "RHG3006",
"inter_ip_address": "1.1.1.1",
"inter_gateway": "1.1.1.2",
"inter_primary_dns": "1.1.1.3",
"inter_secondary_dns": "1.1.1.4",
"inter_firewall": "601036",
"inter_wan_ip_address": "1.1.1.1",
"inter_ipv6_link_local_address": "",
"inter_ipv6_link_global_address": "",
"inter_ipv6_gateway": "",
"inter_ipv6_prefix_delegation": "",
"inter_ipv6_dns_address1": "",
"inter_ipv6_dns_address2": "",
"lan_ip_network": "192.168.0.1/24",
"lan_default_gateway": "192.168.0.1",
"lan_subnet_address_subnet1": "",
"lan_mac_address": "11:22:33:44:55:66",
"lan_dhcp_server": "601036",
"lan_dhcpv6_server": "601036",
"lan_router_advertisement": "601036",
"lan_ipv6_default_gateway": "fe80::1",
"lan_port1_switch_mode": "1301722",
"lan_port2_switch_mode": "1301722",
"lan_port3_switch_mode": "1301722",
"lan_port4_switch_mode": "1301722",
"lan_port1_switch_speed": "10",
"lan_port2_switch_speed": "100",
"lan_port3_switch_speed": "1000",
"lan_port4_switch_speed": "1000",
"lan_port1_switch_status": "1301724",
"lan_port2_switch_status": "1301724",
"lan_port3_switch_status": "1301724",
"lan_port4_switch_status": "1301724",
"wifi_status": "601036",
"wifi_name": "Wifi-Main-Network",
"wifi_mac_address": "AA:BB:CC:DD:EE:FF",
"wifi_security": "401027",
"wifi_channel": "8",
"wifi_bandwidth": "573",
"guest_wifi_status": "601037",
"guest_wifi_name": "Wifi-Guest",
"guest_wifi_mac_addr": "AA:BB:CC:DD:EE:GG",
"guest_wifi_security": "401027",
"guest_wifi_channel": "N/A",
"guest_wifi_ip": "192.168.2.1",
"guest_wifi_subnet_addr": "255.255.255.0",
"guest_wifi_dhcp_server": "192.168.2.1",
"wifi_status_5g": "601036",
"wifi_name_5g": "Wifi-Main-Network",
"wifi_mac_address_5g": "AA:BB:CC:DD:EE:HH",
"wifi_security_5g": "401027",
"wifi_channel_5g": "36",
"wifi_bandwidth_5g": "4803",
"guest_wifi_status_5g": "601037",
"guest_wifi_name_5g": "Wifi-Guest",
"guest_wifi_mac_addr_5g": "AA:BB:CC:DD:EE:II",
"guest_wifi_channel_5g": "N/A",
"guest_wifi_security_5g": "401027",
"guest_wifi_ip_5g": "192.168.2.1",
"guest_wifi_subnet_addr_5g": "255.255.255.0",
"guest_wifi_dhcp_server_5g": "192.168.2.1",
}

View File

@@ -0,0 +1,43 @@
# serializer version: 1
# name: test_entry_diagnostics
dict({
'device_info': dict({
'client_devices': list([
dict({
'connected': True,
'connection_type': 'wifi',
'hostname': 'WifiDevice0',
'type': 'laptop',
}),
]),
'last_exception': None,
'last_update success': True,
'sys_cpu_usage': '97',
'sys_firmware_version': 'XF6_4.0.05.04',
'sys_hardware_version': 'RHG3006 v1',
'sys_memory_usage': '51.94',
'sys_model_name': 'RHG3006',
'sys_reboot_cause': 'Web Reboot',
}),
'entry': dict({
'data': dict({
'host': 'fake_host',
'password': '**REDACTED**',
'username': '**REDACTED**',
}),
'disabled_by': None,
'discovery_keys': dict({
}),
'domain': 'vodafone_station',
'minor_version': 1,
'options': dict({
}),
'pref_disable_new_entities': False,
'pref_disable_polling': False,
'source': 'user',
'title': 'Mock Title',
'unique_id': None,
'version': 1,
}),
})
# ---

View File

@@ -0,0 +1,51 @@
"""Tests for Vodafone Station diagnostics platform."""
from __future__ import annotations
from unittest.mock import patch
from syrupy import SnapshotAssertion
from syrupy.filters import props
from homeassistant.components.vodafone_station.const import DOMAIN
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant
from .const import DEVICE_DATA_QUERY, MOCK_USER_DATA, SENSOR_DATA_QUERY
from tests.common import MockConfigEntry
from tests.components.diagnostics import get_diagnostics_for_config_entry
from tests.typing import ClientSessionGenerator
async def test_entry_diagnostics(
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
snapshot: SnapshotAssertion,
) -> None:
"""Test config entry diagnostics."""
entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA)
entry.add_to_hass(hass)
with (
patch("aiovodafone.api.VodafoneStationSercommApi.login"),
patch(
"aiovodafone.api.VodafoneStationSercommApi.get_devices_data",
return_value=DEVICE_DATA_QUERY,
),
patch(
"aiovodafone.api.VodafoneStationSercommApi.get_sensor_data",
return_value=SENSOR_DATA_QUERY,
),
):
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
assert entry.state == ConfigEntryState.LOADED
assert await get_diagnostics_for_config_entry(hass, hass_client, entry) == snapshot(
exclude=props(
"entry_id",
"created_at",
"modified_at",
)
)

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"
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:

View File

@@ -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(

View File

@@ -146,6 +146,62 @@ async def test_simple_global_timeout_freeze_with_executor_job(
await hass.async_add_executor_job(time.sleep, 0.3)
async def test_simple_global_timeout_does_not_leak_upward(
hass: HomeAssistant,
) -> None:
"""Test a global timeout does not leak upward."""
timeout = TimeoutManager()
current_task = asyncio.current_task()
assert current_task is not None
cancelling_inside_timeout = None
with pytest.raises(asyncio.TimeoutError): # noqa: PT012
async with timeout.async_timeout(0.1):
cancelling_inside_timeout = current_task.cancelling()
await asyncio.sleep(0.3)
assert cancelling_inside_timeout == 0
# After the context manager exits, the task should no longer be cancelling
assert current_task.cancelling() == 0
async def test_simple_global_timeout_does_swallow_cancellation(
hass: HomeAssistant,
) -> None:
"""Test a global timeout does not swallow cancellation."""
timeout = TimeoutManager()
current_task = asyncio.current_task()
assert current_task is not None
cancelling_inside_timeout = None
async def task_with_timeout() -> None:
nonlocal cancelling_inside_timeout
new_task = asyncio.current_task()
assert new_task is not None
with pytest.raises(asyncio.TimeoutError): # noqa: PT012
cancelling_inside_timeout = new_task.cancelling()
async with timeout.async_timeout(0.1):
await asyncio.sleep(0.3)
# After the context manager exits, the task should no longer be cancelling
assert current_task.cancelling() == 0
task = asyncio.create_task(task_with_timeout())
await asyncio.sleep(0)
task.cancel()
assert task.cancelling() == 1
assert cancelling_inside_timeout == 0
# Cancellation should not leak into the current task
assert current_task.cancelling() == 0
# Cancellation should not be swallowed if the task is cancelled
# and it also times out
await asyncio.sleep(0)
with pytest.raises(asyncio.CancelledError):
await task
assert task.cancelling() == 1
async def test_simple_global_timeout_freeze_reset() -> None:
"""Test a simple global timeout freeze reset."""
timeout = TimeoutManager()
@@ -166,6 +222,62 @@ async def test_simple_zone_timeout() -> None:
await asyncio.sleep(0.3)
async def test_simple_zone_timeout_does_not_leak_upward(
hass: HomeAssistant,
) -> None:
"""Test a zone timeout does not leak upward."""
timeout = TimeoutManager()
current_task = asyncio.current_task()
assert current_task is not None
cancelling_inside_timeout = None
with pytest.raises(asyncio.TimeoutError): # noqa: PT012
async with timeout.async_timeout(0.1, "test"):
cancelling_inside_timeout = current_task.cancelling()
await asyncio.sleep(0.3)
assert cancelling_inside_timeout == 0
# After the context manager exits, the task should no longer be cancelling
assert current_task.cancelling() == 0
async def test_simple_zone_timeout_does_swallow_cancellation(
hass: HomeAssistant,
) -> None:
"""Test a zone timeout does not swallow cancellation."""
timeout = TimeoutManager()
current_task = asyncio.current_task()
assert current_task is not None
cancelling_inside_timeout = None
async def task_with_timeout() -> None:
nonlocal cancelling_inside_timeout
new_task = asyncio.current_task()
assert new_task is not None
with pytest.raises(asyncio.TimeoutError): # noqa: PT012
async with timeout.async_timeout(0.1, "test"):
cancelling_inside_timeout = current_task.cancelling()
await asyncio.sleep(0.3)
# After the context manager exits, the task should no longer be cancelling
assert current_task.cancelling() == 0
task = asyncio.create_task(task_with_timeout())
await asyncio.sleep(0)
task.cancel()
assert task.cancelling() == 1
# Cancellation should not leak into the current task
assert cancelling_inside_timeout == 0
assert current_task.cancelling() == 0
# Cancellation should not be swallowed if the task is cancelled
# and it also times out
await asyncio.sleep(0)
with pytest.raises(asyncio.CancelledError):
await task
assert task.cancelling() == 1
async def test_multiple_zone_timeout() -> None:
"""Test a simple zone timeout."""
timeout = TimeoutManager()
@@ -327,7 +439,7 @@ async def test_simple_zone_timeout_freeze_without_timeout_exeption() -> None:
await asyncio.sleep(0.4)
async def test_simple_zone_timeout_zone_with_timeout_exeption() -> None:
async def test_simple_zone_timeout_zone_with_timeout_exception() -> None:
"""Test a simple zone timeout freeze on a zone that does not have a timeout set."""
timeout = TimeoutManager()