This commit is contained in:
Franck Nijhof 2023-06-23 19:08:57 +02:00 committed by GitHub
commit 78222bd51c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
53 changed files with 898 additions and 223 deletions

View file

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

View file

@ -173,7 +173,11 @@ async def async_setup_scanner(
rssi = await hass.async_add_executor_job(client.request_rssi) rssi = await hass.async_add_executor_job(client.request_rssi)
client.close() client.close()
tasks.append(see_device(hass, async_see, mac, friendly_name, rssi)) tasks.append(
asyncio.create_task(
see_device(hass, async_see, mac, friendly_name, rssi)
)
)
if tasks: if tasks:
await asyncio.wait(tasks) await asyncio.wait(tasks)

View file

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive", "documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["bimmer_connected"], "loggers": ["bimmer_connected"],
"requirements": ["bimmer_connected==0.13.6"] "requirements": ["bimmer-connected==0.13.7"]
} }

View file

@ -20,5 +20,5 @@
"dependencies": ["bluetooth_adapters"], "dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/bthome", "documentation": "https://www.home-assistant.io/integrations/bthome",
"iot_class": "local_push", "iot_class": "local_push",
"requirements": ["bthome-ble==2.11.3"] "requirements": ["bthome-ble==2.12.0"]
} }

View file

@ -47,6 +47,15 @@ from .coordinator import (
from .device import device_key_to_bluetooth_entity_key from .device import device_key_to_bluetooth_entity_key
SENSOR_DESCRIPTIONS = { SENSOR_DESCRIPTIONS = {
# Acceleration (m/s²)
(
BTHomeSensorDeviceClass.ACCELERATION,
Units.ACCELERATION_METERS_PER_SQUARE_SECOND,
): SensorEntityDescription(
key=f"{BTHomeSensorDeviceClass.ACCELERATION}_{Units.ACCELERATION_METERS_PER_SQUARE_SECOND}",
native_unit_of_measurement=Units.ACCELERATION_METERS_PER_SQUARE_SECOND,
state_class=SensorStateClass.MEASUREMENT,
),
# Battery (percent) # Battery (percent)
(BTHomeSensorDeviceClass.BATTERY, Units.PERCENTAGE): SensorEntityDescription( (BTHomeSensorDeviceClass.BATTERY, Units.PERCENTAGE): SensorEntityDescription(
key=f"{BTHomeSensorDeviceClass.BATTERY}_{Units.PERCENTAGE}", key=f"{BTHomeSensorDeviceClass.BATTERY}_{Units.PERCENTAGE}",
@ -131,6 +140,15 @@ SENSOR_DESCRIPTIONS = {
native_unit_of_measurement=UnitOfVolume.CUBIC_METERS, native_unit_of_measurement=UnitOfVolume.CUBIC_METERS,
state_class=SensorStateClass.TOTAL, state_class=SensorStateClass.TOTAL,
), ),
# Gyroscope (°/s)
(
BTHomeSensorDeviceClass.GYROSCOPE,
Units.GYROSCOPE_DEGREES_PER_SECOND,
): SensorEntityDescription(
key=f"{BTHomeSensorDeviceClass.GYROSCOPE}_{Units.GYROSCOPE_DEGREES_PER_SECOND}",
native_unit_of_measurement=Units.GYROSCOPE_DEGREES_PER_SECOND,
state_class=SensorStateClass.MEASUREMENT,
),
# Humidity in (percent) # Humidity in (percent)
(BTHomeSensorDeviceClass.HUMIDITY, Units.PERCENTAGE): SensorEntityDescription( (BTHomeSensorDeviceClass.HUMIDITY, Units.PERCENTAGE): SensorEntityDescription(
key=f"{BTHomeSensorDeviceClass.HUMIDITY}_{Units.PERCENTAGE}", key=f"{BTHomeSensorDeviceClass.HUMIDITY}_{Units.PERCENTAGE}",
@ -242,6 +260,15 @@ SENSOR_DESCRIPTIONS = {
native_unit_of_measurement=UnitOfTemperature.CELSIUS, native_unit_of_measurement=UnitOfTemperature.CELSIUS,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
# Timestamp (datetime object)
(
BTHomeSensorDeviceClass.TIMESTAMP,
None,
): SensorEntityDescription(
key=f"{BTHomeSensorDeviceClass.TIMESTAMP}",
device_class=SensorDeviceClass.TIMESTAMP,
state_class=SensorStateClass.MEASUREMENT,
),
# UV index (-) # UV index (-)
( (
BTHomeSensorDeviceClass.UV_INDEX, BTHomeSensorDeviceClass.UV_INDEX,

View file

@ -43,7 +43,7 @@ def get_scanner(hass: HomeAssistant, config: ConfigType) -> FortiOSDeviceScanner
fgt = FortiOSAPI() fgt = FortiOSAPI()
try: try:
fgt.tokenlogin(host, token, verify_ssl) fgt.tokenlogin(host, token, verify_ssl, None, 12, "root")
except ConnectionError as ex: except ConnectionError as ex:
_LOGGER.error("ConnectionError to FortiOS API: %s", ex) _LOGGER.error("ConnectionError to FortiOS API: %s", ex)
return None return None
@ -77,7 +77,12 @@ class FortiOSDeviceScanner(DeviceScanner):
def update(self): def update(self):
"""Update clients from the device.""" """Update clients from the device."""
clients_json = self._fgt.monitor("user/device/query", "") clients_json = self._fgt.monitor(
"user/device/query",
"",
parameters={"filter": "format=master_mac|hostname|is_online"},
)
self._clients_json = clients_json self._clients_json = clients_json
self._clients = [] self._clients = []
@ -85,8 +90,12 @@ class FortiOSDeviceScanner(DeviceScanner):
if clients_json: if clients_json:
try: try:
for client in clients_json["results"]: for client in clients_json["results"]:
if client["is_online"]: if (
self._clients.append(client["mac"].upper()) "is_online" in client
and "master_mac" in client
and client["is_online"]
):
self._clients.append(client["master_mac"].upper())
except KeyError as kex: except KeyError as kex:
_LOGGER.error("Key not found in clients: %s", kex) _LOGGER.error("Key not found in clients: %s", kex)
@ -106,17 +115,10 @@ class FortiOSDeviceScanner(DeviceScanner):
return None return None
for client in data["results"]: for client in data["results"]:
if client["mac"] == device: if "master_mac" in client and client["master_mac"] == device:
try: if "hostname" in client:
name = client["hostname"] name = client["hostname"]
_LOGGER.debug("Getting device name=%s", name) else:
name = client["master_mac"].replace(":", "_")
return name return name
except KeyError as kex:
_LOGGER.debug(
"No hostname found for %s in client data: %s",
device,
kex,
)
return device.replace(":", "_")
return None return None

View file

@ -2,6 +2,7 @@
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform from homeassistant.const import Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.typing import ConfigType
from .const import DOMAIN from .const import DOMAIN
from .coordinator import FullyKioskDataUpdateCoordinator from .coordinator import FullyKioskDataUpdateCoordinator
@ -17,6 +18,14 @@ PLATFORMS = [
] ]
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up Fully Kiosk Browser."""
await async_setup_services(hass)
return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Fully Kiosk Browser from a config entry.""" """Set up Fully Kiosk Browser from a config entry."""
@ -28,8 +37,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
coordinator.async_update_listeners() coordinator.async_update_listeners()
await async_setup_services(hass)
return True return True

View file

@ -1,14 +1,12 @@
"""Services for the Fully Kiosk Browser integration.""" """Services for the Fully Kiosk Browser integration."""
from __future__ import annotations from __future__ import annotations
from collections.abc import Callable
from typing import Any
from fullykiosk import FullyKiosk
import voluptuous as vol import voluptuous as vol
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
from homeassistant.const import ATTR_DEVICE_ID from homeassistant.const import ATTR_DEVICE_ID
from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.exceptions import HomeAssistantError
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
import homeassistant.helpers.device_registry as dr import homeassistant.helpers.device_registry as dr
@ -16,59 +14,53 @@ from .const import (
ATTR_APPLICATION, ATTR_APPLICATION,
ATTR_URL, ATTR_URL,
DOMAIN, DOMAIN,
LOGGER,
SERVICE_LOAD_URL, SERVICE_LOAD_URL,
SERVICE_START_APPLICATION, SERVICE_START_APPLICATION,
) )
from .coordinator import FullyKioskDataUpdateCoordinator
async def async_setup_services(hass: HomeAssistant) -> None: async def async_setup_services(hass: HomeAssistant) -> None:
"""Set up the services for the Fully Kiosk Browser integration.""" """Set up the services for the Fully Kiosk Browser integration."""
async def execute_service( async def collect_coordinators(
call: ServiceCall, device_ids: list[str],
fully_method: Callable, ) -> list[FullyKioskDataUpdateCoordinator]:
*args: list[str], config_entries = list[ConfigEntry]()
**kwargs: dict[str, Any],
) -> None:
"""Execute a Fully service call.
:param call: {ServiceCall} HA service call.
:param fully_method: {Callable} A method of the FullyKiosk class.
:param args: Arguments for fully_method.
:param kwargs: Key-word arguments for fully_method.
:return: None
"""
LOGGER.debug(
"Calling Fully service %s with args: %s, %s", ServiceCall, args, kwargs
)
registry = dr.async_get(hass) registry = dr.async_get(hass)
for target in call.data[ATTR_DEVICE_ID]: for target in device_ids:
device = registry.async_get(target) device = registry.async_get(target)
if device: if device:
for key in device.config_entries: device_entries = list[ConfigEntry]()
entry = hass.config_entries.async_get_entry(key) for entry_id in device.config_entries:
if not entry: entry = hass.config_entries.async_get_entry(entry_id)
continue if entry and entry.domain == DOMAIN:
if entry.domain != DOMAIN: device_entries.append(entry)
continue if not device_entries:
coordinator = hass.data[DOMAIN][key] raise HomeAssistantError(
# fully_method(coordinator.fully, *args, **kwargs) would make f"Device '{target}' is not a {DOMAIN} device"
# test_services.py fail.
await getattr(coordinator.fully, fully_method.__name__)(
*args, **kwargs
) )
break config_entries.extend(device_entries)
else:
raise HomeAssistantError(
f"Device '{target}' not found in device registry"
)
coordinators = list[FullyKioskDataUpdateCoordinator]()
for config_entry in config_entries:
if config_entry.state != ConfigEntryState.LOADED:
raise HomeAssistantError(f"{config_entry.title} is not loaded")
coordinators.append(hass.data[DOMAIN][config_entry.entry_id])
return coordinators
async def async_load_url(call: ServiceCall) -> None: async def async_load_url(call: ServiceCall) -> None:
"""Load a URL on the Fully Kiosk Browser.""" """Load a URL on the Fully Kiosk Browser."""
await execute_service(call, FullyKiosk.loadUrl, call.data[ATTR_URL]) for coordinator in await collect_coordinators(call.data[ATTR_DEVICE_ID]):
await coordinator.fully.loadUrl(call.data[ATTR_URL])
async def async_start_app(call: ServiceCall) -> None: async def async_start_app(call: ServiceCall) -> None:
"""Start an app on the device.""" """Start an app on the device."""
await execute_service( for coordinator in await collect_coordinators(call.data[ATTR_DEVICE_ID]):
call, FullyKiosk.startApplication, call.data[ATTR_APPLICATION] await coordinator.fully.startApplication(call.data[ATTR_APPLICATION])
)
# Register all the above services # Register all the above services
service_mapping = [ service_mapping = [

View file

@ -223,13 +223,6 @@ SENSOR_TYPES = {
icon="mdi:docker", icon="mdi:docker",
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
("raid", "used"): GlancesSensorEntityDescription(
key="used",
type="raid",
name_suffix="Raid used",
icon="mdi:harddisk",
state_class=SensorStateClass.MEASUREMENT,
),
("raid", "available"): GlancesSensorEntityDescription( ("raid", "available"): GlancesSensorEntityDescription(
key="available", key="available",
type="raid", type="raid",
@ -237,6 +230,13 @@ SENSOR_TYPES = {
icon="mdi:harddisk", icon="mdi:harddisk",
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
("raid", "used"): GlancesSensorEntityDescription(
key="used",
type="raid",
name_suffix="Raid used",
icon="mdi:harddisk",
state_class=SensorStateClass.MEASUREMENT,
),
} }
@ -269,7 +269,7 @@ async def async_setup_entry(
if sensor_type in ["fs", "sensors", "raid"]: if sensor_type in ["fs", "sensors", "raid"]:
for sensor_label, params in sensors.items(): for sensor_label, params in sensors.items():
for param in params: for param in params:
sensor_description = SENSOR_TYPES[(sensor_type, param)] if sensor_description := SENSOR_TYPES.get((sensor_type, param)):
_migrate_old_unique_ids( _migrate_old_unique_ids(
hass, hass,
f"{coordinator.host}-{name} {sensor_label} {sensor_description.name_suffix}", f"{coordinator.host}-{name} {sensor_label} {sensor_description.name_suffix}",
@ -285,7 +285,7 @@ async def async_setup_entry(
) )
else: else:
for sensor in sensors: for sensor in sensors:
sensor_description = SENSOR_TYPES[(sensor_type, sensor)] if sensor_description := SENSOR_TYPES.get((sensor_type, sensor)):
_migrate_old_unique_ids( _migrate_old_unique_ids(
hass, hass,
f"{coordinator.host}-{name} {sensor_description.name_suffix}", f"{coordinator.host}-{name} {sensor_description.name_suffix}",

View file

@ -243,7 +243,7 @@ class InverterSensor(CoordinatorEntity[GoodweUpdateCoordinator], SensorEntity):
In contrast to "total" sensors, these "daily" sensors need to be reset to 0 on midnight. In contrast to "total" sensors, these "daily" sensors need to be reset to 0 on midnight.
""" """
if not self.coordinator.last_update_success: if not self.coordinator.last_update_success:
self.coordinator.reset_sensor(self._sensor.id) self.coordinator.reset_sensor(self._sensor.id_)
self.async_write_ha_state() self.async_write_ha_state()
_LOGGER.debug("Goodwe reset %s to 0", self.name) _LOGGER.debug("Goodwe reset %s to 0", self.name)
next_midnight = dt_util.start_of_local_day( next_midnight = dt_util.start_of_local_day(

View file

@ -24,7 +24,7 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15)
SensorType = namedtuple("SensorType", ["name", "icon", "unit", "path"]) SensorType = namedtuple("SensorType", ["name", "icon", "unit", "path"])
SENSORS_TYPES = { SENSORS_TYPES = {
"name": SensorType("Name", None, "", ["profile", "name"]), "name": SensorType("Name", None, None, ["profile", "name"]),
"hp": SensorType("HP", "mdi:heart", "HP", ["stats", "hp"]), "hp": SensorType("HP", "mdi:heart", "HP", ["stats", "hp"]),
"maxHealth": SensorType("max HP", "mdi:heart", "HP", ["stats", "maxHealth"]), "maxHealth": SensorType("max HP", "mdi:heart", "HP", ["stats", "maxHealth"]),
"mp": SensorType("Mana", "mdi:auto-fix", "MP", ["stats", "mp"]), "mp": SensorType("Mana", "mdi:auto-fix", "MP", ["stats", "mp"]),
@ -35,7 +35,7 @@ SENSORS_TYPES = {
"Lvl", "mdi:arrow-up-bold-circle-outline", "Lvl", ["stats", "lvl"] "Lvl", "mdi:arrow-up-bold-circle-outline", "Lvl", ["stats", "lvl"]
), ),
"gp": SensorType("Gold", "mdi:circle-multiple", "Gold", ["stats", "gp"]), "gp": SensorType("Gold", "mdi:circle-multiple", "Gold", ["stats", "gp"]),
"class": SensorType("Class", "mdi:sword", "", ["stats", "class"]), "class": SensorType("Class", "mdi:sword", None, ["stats", "class"]),
} }
TASKS_TYPES = { TASKS_TYPES = {

View file

@ -305,7 +305,11 @@ class SupervisorIssues:
async def update(self) -> None: async def update(self) -> None:
"""Update issues from Supervisor resolution center.""" """Update issues from Supervisor resolution center."""
try:
data = await self._client.get_resolution_info() data = await self._client.get_resolution_info()
except HassioAPIError as err:
_LOGGER.error("Failed to update supervisor issues: %r", err)
return
self.unhealthy_reasons = set(data[ATTR_UNHEALTHY]) self.unhealthy_reasons = set(data[ATTR_UNHEALTHY])
self.unsupported_reasons = set(data[ATTR_UNSUPPORTED]) self.unsupported_reasons = set(data[ATTR_UNSUPPORTED])

View file

@ -626,10 +626,10 @@ class HomeDriver(AccessoryDriver): # type: ignore[misc]
@pyhap_callback # type: ignore[misc] @pyhap_callback # type: ignore[misc]
def pair( def pair(
self, client_uuid: UUID, client_public: str, client_permissions: int self, client_username_bytes: bytes, client_public: str, client_permissions: int
) -> bool: ) -> bool:
"""Override super function to dismiss setup message if paired.""" """Override super function to dismiss setup message if paired."""
success = super().pair(client_uuid, client_public, client_permissions) success = super().pair(client_username_bytes, client_public, client_permissions)
if success: if success:
async_dismiss_setup_message(self.hass, self._entry_id) async_dismiss_setup_message(self.hass, self._entry_id)
return cast(bool, success) return cast(bool, success)

View file

@ -9,7 +9,7 @@
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["pyhap"], "loggers": ["pyhap"],
"requirements": [ "requirements": [
"HAP-python==4.6.0", "HAP-python==4.7.0",
"fnv-hash-fast==0.3.1", "fnv-hash-fast==0.3.1",
"PyQRCode==1.2.1", "PyQRCode==1.2.1",
"base36==0.1.1" "base36==0.1.1"

View file

@ -115,8 +115,8 @@ def add_insteon_events(hass: HomeAssistant, device: Device) -> None:
"""Register Insteon device events.""" """Register Insteon device events."""
@callback @callback
def async_fire_group_on_off_event( def async_fire_insteon_event(
name: str, address: Address, group: int, button: str name: str, address: Address, group: int, button: str | None = None
): ):
# Firing an event when a button is pressed. # Firing an event when a button is pressed.
if button and button[-2] == "_": if button and button[-2] == "_":
@ -146,9 +146,9 @@ def add_insteon_events(hass: HomeAssistant, device: Device) -> None:
for name_or_group, event in device.events.items(): for name_or_group, event in device.events.items():
if isinstance(name_or_group, int): if isinstance(name_or_group, int):
for _, event in device.events[name_or_group].items(): for _, event in device.events[name_or_group].items():
_register_event(event, async_fire_group_on_off_event) _register_event(event, async_fire_insteon_event)
else: else:
_register_event(event, async_fire_group_on_off_event) _register_event(event, async_fire_insteon_event)
def register_new_device_callback(hass): def register_new_device_callback(hass):

View file

@ -84,6 +84,7 @@ async def async_attach_trigger(
trigger_info: TriggerInfo, trigger_info: TriggerInfo,
) -> CALLBACK_TYPE: ) -> CALLBACK_TYPE:
"""Attach a trigger.""" """Attach a trigger."""
trigger_data = trigger_info["trigger_data"]
dst_addresses: list[str] = config.get(EXTRA_FIELD_DESTINATION, []) dst_addresses: list[str] = config.get(EXTRA_FIELD_DESTINATION, [])
job = HassJob(action, f"KNX device trigger {trigger_info}") job = HassJob(action, f"KNX device trigger {trigger_info}")
knx: KNXModule = hass.data[DOMAIN] knx: KNXModule = hass.data[DOMAIN]
@ -95,7 +96,7 @@ async def async_attach_trigger(
return return
hass.async_run_hass_job( hass.async_run_hass_job(
job, job,
{"trigger": telegram}, {"trigger": {**trigger_data, **telegram}},
) )
return knx.telegrams.async_listen_telegram( return knx.telegrams.async_listen_telegram(

View file

@ -3,7 +3,7 @@ from __future__ import annotations
import hashlib import hashlib
from pylast import LastFMNetwork, Track, User, WSError from pylast import LastFMNetwork, PyLastError, Track, User
import voluptuous as vol import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
@ -104,26 +104,30 @@ class LastFmSensor(SensorEntity):
def update(self) -> None: def update(self) -> None:
"""Update device state.""" """Update device state."""
self._attr_native_value = STATE_NOT_SCROBBLING
try: try:
self._user.get_playcount() play_count = self._user.get_playcount()
except WSError as exc: self._attr_entity_picture = self._user.get_image()
now_playing = self._user.get_now_playing()
top_tracks = self._user.get_top_tracks(limit=1)
last_tracks = self._user.get_recent_tracks(limit=1)
except PyLastError as exc:
self._attr_available = False self._attr_available = False
LOGGER.error("Failed to load LastFM user `%s`: %r", self._user.name, exc) LOGGER.error("Failed to load LastFM user `%s`: %r", self._user.name, exc)
return return
self._attr_entity_picture = self._user.get_image() self._attr_available = True
if now_playing := self._user.get_now_playing(): if now_playing:
self._attr_native_value = format_track(now_playing) self._attr_native_value = format_track(now_playing)
else:
self._attr_native_value = STATE_NOT_SCROBBLING
top_played = None
if top_tracks := self._user.get_top_tracks(limit=1):
top_played = format_track(top_tracks[0].item)
last_played = None
if last_tracks := self._user.get_recent_tracks(limit=1):
last_played = format_track(last_tracks[0].track)
play_count = self._user.get_playcount()
self._attr_extra_state_attributes = { self._attr_extra_state_attributes = {
ATTR_LAST_PLAYED: last_played,
ATTR_PLAY_COUNT: play_count, ATTR_PLAY_COUNT: play_count,
ATTR_TOP_PLAYED: top_played, ATTR_LAST_PLAYED: None,
ATTR_TOP_PLAYED: None,
} }
if len(last_tracks) > 0:
self._attr_extra_state_attributes[ATTR_LAST_PLAYED] = format_track(
last_tracks[0].track
)
if len(top_tracks) > 0:
self._attr_extra_state_attributes[ATTR_TOP_PLAYED] = format_track(
top_tracks[0].item
)

View file

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/local_calendar", "documentation": "https://www.home-assistant.io/integrations/local_calendar",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["ical"], "loggers": ["ical"],
"requirements": ["ical==4.5.1"] "requirements": ["ical==4.5.4"]
} }

View file

@ -2,11 +2,12 @@
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
from contextlib import suppress
import async_timeout import async_timeout
from matter_server.client import MatterClient from matter_server.client import MatterClient
from matter_server.client.exceptions import CannotConnect, InvalidServerVersion from matter_server.client.exceptions import CannotConnect, InvalidServerVersion
from matter_server.common.errors import MatterError, NodeCommissionFailed from matter_server.common.errors import MatterError, NodeCommissionFailed, NodeNotExists
import voluptuous as vol import voluptuous as vol
from homeassistant.components.hassio import AddonError, AddonManager, AddonState from homeassistant.components.hassio import AddonError, AddonManager, AddonState
@ -207,6 +208,8 @@ async def async_remove_config_entry_device(
) )
matter = get_matter(hass) matter = get_matter(hass)
with suppress(NodeNotExists):
# ignore if the server has already removed the node.
await matter.matter_client.remove_node(node.node_id) await matter.matter_client.remove_node(node.node_id)
return True return True

View file

@ -92,7 +92,7 @@ class MatterAdapter:
get_clean_name(basic_info.nodeLabel) get_clean_name(basic_info.nodeLabel)
or get_clean_name(basic_info.productLabel) or get_clean_name(basic_info.productLabel)
or get_clean_name(basic_info.productName) or get_clean_name(basic_info.productName)
or device_type.__class__.__name__ or device_type.__name__
if device_type if device_type
else None else None
) )
@ -117,7 +117,7 @@ class MatterAdapter:
identifiers.add((DOMAIN, f"{ID_TYPE_SERIAL}_{basic_info.serialNumber}")) identifiers.add((DOMAIN, f"{ID_TYPE_SERIAL}_{basic_info.serialNumber}"))
model = ( model = (
get_clean_name(basic_info.productName) or device_type.__class__.__name__ get_clean_name(basic_info.productName) or device_type.__name__
if device_type if device_type
else None else None
) )

View file

@ -6,5 +6,5 @@
"dependencies": ["websocket_api"], "dependencies": ["websocket_api"],
"documentation": "https://www.home-assistant.io/integrations/matter", "documentation": "https://www.home-assistant.io/integrations/matter",
"iot_class": "local_push", "iot_class": "local_push",
"requirements": ["python-matter-server==3.4.1"] "requirements": ["python-matter-server==3.5.1"]
} }

View file

@ -133,9 +133,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
await coordinator_alert.async_refresh() await coordinator_alert.async_refresh()
if not coordinator_alert.last_update_success: if coordinator_alert.last_update_success:
raise ConfigEntryNotReady
hass.data[DOMAIN][department] = True hass.data[DOMAIN][department] = True
else: else:
_LOGGER.warning( _LOGGER.warning(
@ -158,11 +156,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
undo_listener = entry.add_update_listener(_async_update_listener) undo_listener = entry.add_update_listener(_async_update_listener)
hass.data[DOMAIN][entry.entry_id] = { hass.data[DOMAIN][entry.entry_id] = {
UNDO_UPDATE_LISTENER: undo_listener,
COORDINATOR_FORECAST: coordinator_forecast, COORDINATOR_FORECAST: coordinator_forecast,
COORDINATOR_RAIN: coordinator_rain, COORDINATOR_RAIN: coordinator_rain,
COORDINATOR_ALERT: coordinator_alert,
UNDO_UPDATE_LISTENER: undo_listener,
} }
if coordinator_alert and coordinator_alert.last_update_success:
hass.data[DOMAIN][entry.entry_id][COORDINATOR_ALERT] = coordinator_alert
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)

View file

@ -16,5 +16,5 @@
"dependencies": ["bluetooth_adapters"], "dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/rapt_ble", "documentation": "https://www.home-assistant.io/integrations/rapt_ble",
"iot_class": "local_push", "iot_class": "local_push",
"requirements": ["rapt-ble==0.1.1"] "requirements": ["rapt-ble==0.1.2"]
} }

View file

@ -81,16 +81,10 @@ class TotalConnectZoneSecurityBinarySensor(TotalConnectZoneBinarySensor):
return BinarySensorDeviceClass.MOTION return BinarySensorDeviceClass.MOTION
if self._zone.is_type_medical(): if self._zone.is_type_medical():
return BinarySensorDeviceClass.SAFETY return BinarySensorDeviceClass.SAFETY
# "security" type is a generic category so test for it last if self._zone.is_type_temperature():
if self._zone.is_type_security(): return BinarySensorDeviceClass.PROBLEM
return BinarySensorDeviceClass.DOOR return BinarySensorDeviceClass.DOOR
_LOGGER.error(
"TotalConnect zone %s reported an unexpected device class",
self._zone.zoneid,
)
return None
def update(self): def update(self):
"""Return the state of the device.""" """Return the state of the device."""
if self._zone.is_faulted() or self._zone.is_triggered(): if self._zone.is_faulted() or self._zone.is_triggered():

View file

@ -85,9 +85,16 @@ class YouTubeDataUpdateCoordinator(DataUpdateCoordinator):
ATTR_PUBLISHED_AT: video["snippet"]["publishedAt"], ATTR_PUBLISHED_AT: video["snippet"]["publishedAt"],
ATTR_TITLE: video["snippet"]["title"], ATTR_TITLE: video["snippet"]["title"],
ATTR_DESCRIPTION: video["snippet"]["description"], ATTR_DESCRIPTION: video["snippet"]["description"],
ATTR_THUMBNAIL: video["snippet"]["thumbnails"]["standard"]["url"], ATTR_THUMBNAIL: self._get_thumbnail(video),
ATTR_VIDEO_ID: video["contentDetails"]["videoId"], ATTR_VIDEO_ID: video["contentDetails"]["videoId"],
}, },
ATTR_SUBSCRIBER_COUNT: int(channel["statistics"]["subscriberCount"]), ATTR_SUBSCRIBER_COUNT: int(channel["statistics"]["subscriberCount"]),
} }
return data return data
def _get_thumbnail(self, video: dict[str, Any]) -> str | None:
thumbnails = video["snippet"]["thumbnails"]
for size in ("standard", "high", "medium", "default"):
if size in thumbnails:
return thumbnails[size]["url"]
return None

View file

@ -30,7 +30,7 @@ class YouTubeMixin:
"""Mixin for required keys.""" """Mixin for required keys."""
value_fn: Callable[[Any], StateType] value_fn: Callable[[Any], StateType]
entity_picture_fn: Callable[[Any], str] entity_picture_fn: Callable[[Any], str | None]
attributes_fn: Callable[[Any], dict[str, Any]] | None attributes_fn: Callable[[Any], dict[str, Any]] | None
@ -87,7 +87,7 @@ class YouTubeSensor(YouTubeChannelEntity, SensorEntity):
return self.entity_description.value_fn(self.coordinator.data[self._channel_id]) return self.entity_description.value_fn(self.coordinator.data[self._channel_id])
@property @property
def entity_picture(self) -> str: def entity_picture(self) -> str | None:
"""Return the value reported by the sensor.""" """Return the value reported by the sensor."""
return self.entity_description.entity_picture_fn( return self.entity_description.entity_picture_fn(
self.coordinator.data[self._channel_id] self.coordinator.data[self._channel_id]

View file

@ -17,6 +17,10 @@
"data": { "data": {
"channels": "YouTube channels" "channels": "YouTube channels"
} }
},
"reauth_confirm": {
"title": "[%key:common::config_flow::title::reauth%]",
"description": "The YouTube integration needs to re-authenticate your account"
} }
} }
}, },

View file

@ -65,7 +65,7 @@ def redact_node_state(node_state: NodeDataType) -> NodeDataType:
def get_device_entities( def get_device_entities(
hass: HomeAssistant, node: Node, device: dr.DeviceEntry hass: HomeAssistant, node: Node, config_entry: ConfigEntry, device: dr.DeviceEntry
) -> list[dict[str, Any]]: ) -> list[dict[str, Any]]:
"""Get entities for a device.""" """Get entities for a device."""
entity_entries = er.async_entries_for_device( entity_entries = er.async_entries_for_device(
@ -73,6 +73,10 @@ def get_device_entities(
) )
entities = [] entities = []
for entry in entity_entries: for entry in entity_entries:
# Skip entities that are not part of this integration
if entry.config_entry_id != config_entry.entry_id:
continue
# If the value ID returns as None, we don't need to include this entity # If the value ID returns as None, we don't need to include this entity
if (value_id := get_value_id_from_unique_id(entry.unique_id)) is None: if (value_id := get_value_id_from_unique_id(entry.unique_id)) is None:
continue continue
@ -142,7 +146,7 @@ async def async_get_device_diagnostics(
if node_id is None or node_id not in driver.controller.nodes: if node_id is None or node_id not in driver.controller.nodes:
raise ValueError(f"Node for device {device.id} can't be found") raise ValueError(f"Node for device {device.id} can't be found")
node = driver.controller.nodes[node_id] node = driver.controller.nodes[node_id]
entities = get_device_entities(hass, node, device) entities = get_device_entities(hass, node, config_entry, device)
assert client.version assert client.version
node_state = redact_node_state(async_redact_data(node.data, KEYS_TO_REDACT)) node_state = redact_node_state(async_redact_data(node.data, KEYS_TO_REDACT))
return { return {

View file

@ -142,8 +142,9 @@ async def async_attach_trigger(
) -> CALLBACK_TYPE: ) -> CALLBACK_TYPE:
"""Listen for state changes based on configuration.""" """Listen for state changes based on configuration."""
dev_reg = dr.async_get(hass) dev_reg = dr.async_get(hass)
nodes = async_get_nodes_from_targets(hass, config, dev_reg=dev_reg) if config[ATTR_EVENT_SOURCE] == "node" and not async_get_nodes_from_targets(
if config[ATTR_EVENT_SOURCE] == "node" and not nodes: hass, config, dev_reg=dev_reg
):
raise ValueError( raise ValueError(
f"No nodes found for given {ATTR_DEVICE_ID}s or {ATTR_ENTITY_ID}s." f"No nodes found for given {ATTR_DEVICE_ID}s or {ATTR_ENTITY_ID}s."
) )
@ -215,7 +216,7 @@ async def async_attach_trigger(
# Nodes list can come from different drivers and we will need to listen to # Nodes list can come from different drivers and we will need to listen to
# server connections for all of them. # server connections for all of them.
drivers: set[Driver] = set() drivers: set[Driver] = set()
if not nodes: if not (nodes := async_get_nodes_from_targets(hass, config, dev_reg=dev_reg)):
entry_id = config[ATTR_CONFIG_ENTRY_ID] entry_id = config[ATTR_CONFIG_ENTRY_ID]
client: Client = hass.data[DOMAIN][entry_id][DATA_CLIENT] client: Client = hass.data[DOMAIN][entry_id][DATA_CLIENT]
driver = client.driver driver = client.driver

View file

@ -91,7 +91,7 @@ async def async_attach_trigger(
) -> CALLBACK_TYPE: ) -> CALLBACK_TYPE:
"""Listen for state changes based on configuration.""" """Listen for state changes based on configuration."""
dev_reg = dr.async_get(hass) dev_reg = dr.async_get(hass)
if not (nodes := async_get_nodes_from_targets(hass, config, dev_reg=dev_reg)): if not async_get_nodes_from_targets(hass, config, dev_reg=dev_reg):
raise ValueError( raise ValueError(
f"No nodes found for given {ATTR_DEVICE_ID}s or {ATTR_ENTITY_ID}s." f"No nodes found for given {ATTR_DEVICE_ID}s or {ATTR_ENTITY_ID}s."
) )
@ -174,7 +174,7 @@ async def async_attach_trigger(
# Nodes list can come from different drivers and we will need to listen to # Nodes list can come from different drivers and we will need to listen to
# server connections for all of them. # server connections for all of them.
drivers: set[Driver] = set() drivers: set[Driver] = set()
for node in nodes: for node in async_get_nodes_from_targets(hass, config, dev_reg=dev_reg):
driver = node.client.driver driver = node.client.driver
assert driver is not None # The node comes from the driver. assert driver is not None # The node comes from the driver.
drivers.add(driver) drivers.add(driver)

View file

@ -8,7 +8,7 @@ from .backports.enum import StrEnum
APPLICATION_NAME: Final = "HomeAssistant" APPLICATION_NAME: Final = "HomeAssistant"
MAJOR_VERSION: Final = 2023 MAJOR_VERSION: Final = 2023
MINOR_VERSION: Final = 6 MINOR_VERSION: Final = 6
PATCH_VERSION: Final = "2" PATCH_VERSION: Final = "3"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 10, 0) REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 10, 0)

View file

@ -46,7 +46,7 @@ pyyaml==6.0
requests==2.31.0 requests==2.31.0
scapy==2.5.0 scapy==2.5.0
sqlalchemy==2.0.15 sqlalchemy==2.0.15
typing-extensions>=4.5.0,<5.0 typing_extensions>=4.6.3,<5.0
ulid-transform==0.7.2 ulid-transform==0.7.2
voluptuous-serialize==2.6.0 voluptuous-serialize==2.6.0
voluptuous==0.13.1 voluptuous==0.13.1

View file

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "homeassistant" name = "homeassistant"
version = "2023.6.2" version = "2023.6.3"
license = {text = "Apache-2.0"} license = {text = "Apache-2.0"}
description = "Open-source home automation platform running on Python 3." description = "Open-source home automation platform running on Python 3."
readme = "README.rst" readme = "README.rst"
@ -49,7 +49,7 @@ dependencies = [
"python-slugify==4.0.1", "python-slugify==4.0.1",
"pyyaml==6.0", "pyyaml==6.0",
"requests==2.31.0", "requests==2.31.0",
"typing-extensions>=4.5.0,<5.0", "typing_extensions>=4.6.3,<5.0",
"ulid-transform==0.7.2", "ulid-transform==0.7.2",
"voluptuous==0.13.1", "voluptuous==0.13.1",
"voluptuous-serialize==2.6.0", "voluptuous-serialize==2.6.0",

View file

@ -23,7 +23,7 @@ pip>=21.0,<23.2
python-slugify==4.0.1 python-slugify==4.0.1
pyyaml==6.0 pyyaml==6.0
requests==2.31.0 requests==2.31.0
typing-extensions>=4.5.0,<5.0 typing_extensions>=4.6.3,<5.0
ulid-transform==0.7.2 ulid-transform==0.7.2
voluptuous==0.13.1 voluptuous==0.13.1
voluptuous-serialize==2.6.0 voluptuous-serialize==2.6.0

View file

@ -11,7 +11,7 @@ AIOAladdinConnect==0.1.56
Adax-local==0.1.5 Adax-local==0.1.5
# homeassistant.components.homekit # homeassistant.components.homekit
HAP-python==4.6.0 HAP-python==4.7.0
# homeassistant.components.mastodon # homeassistant.components.mastodon
Mastodon.py==1.5.1 Mastodon.py==1.5.1
@ -119,7 +119,7 @@ aioairq==0.2.4
aioairzone-cloud==0.1.8 aioairzone-cloud==0.1.8
# homeassistant.components.airzone # homeassistant.components.airzone
aioairzone==0.6.3 aioairzone==0.6.4
# homeassistant.components.ambient_station # homeassistant.components.ambient_station
aioambient==2023.04.0 aioambient==2023.04.0
@ -434,7 +434,7 @@ beautifulsoup4==4.11.1
bellows==0.35.5 bellows==0.35.5
# homeassistant.components.bmw_connected_drive # homeassistant.components.bmw_connected_drive
bimmer_connected==0.13.6 bimmer-connected==0.13.7
# homeassistant.components.bizkaibus # homeassistant.components.bizkaibus
bizkaibus==0.1.1 bizkaibus==0.1.1
@ -502,7 +502,7 @@ brunt==1.2.0
bt_proximity==0.2.1 bt_proximity==0.2.1
# homeassistant.components.bthome # homeassistant.components.bthome
bthome-ble==2.11.3 bthome-ble==2.12.0
# homeassistant.components.bt_home_hub_5 # homeassistant.components.bt_home_hub_5
bthomehub5-devicelist==0.1.1 bthomehub5-devicelist==0.1.1
@ -966,7 +966,7 @@ ibeacon_ble==1.0.1
ibmiotf==0.3.4 ibmiotf==0.3.4
# homeassistant.components.local_calendar # homeassistant.components.local_calendar
ical==4.5.1 ical==4.5.4
# homeassistant.components.ping # homeassistant.components.ping
icmplib==3.0 icmplib==3.0
@ -2087,7 +2087,7 @@ python-kasa==0.5.1
# python-lirc==1.2.3 # python-lirc==1.2.3
# homeassistant.components.matter # homeassistant.components.matter
python-matter-server==3.4.1 python-matter-server==3.5.1
# homeassistant.components.xiaomi_miio # homeassistant.components.xiaomi_miio
python-miio==0.5.12 python-miio==0.5.12
@ -2240,7 +2240,7 @@ radiotherm==2.1.0
raincloudy==0.0.7 raincloudy==0.0.7
# homeassistant.components.rapt_ble # homeassistant.components.rapt_ble
rapt-ble==0.1.1 rapt-ble==0.1.2
# homeassistant.components.raspyrfm # homeassistant.components.raspyrfm
raspyrfm-client==1.2.8 raspyrfm-client==1.2.8

View file

@ -13,7 +13,7 @@ AIOAladdinConnect==0.1.56
Adax-local==0.1.5 Adax-local==0.1.5
# homeassistant.components.homekit # homeassistant.components.homekit
HAP-python==4.6.0 HAP-python==4.7.0
# homeassistant.components.flick_electric # homeassistant.components.flick_electric
PyFlick==0.0.2 PyFlick==0.0.2
@ -109,7 +109,7 @@ aioairq==0.2.4
aioairzone-cloud==0.1.8 aioairzone-cloud==0.1.8
# homeassistant.components.airzone # homeassistant.components.airzone
aioairzone==0.6.3 aioairzone==0.6.4
# homeassistant.components.ambient_station # homeassistant.components.ambient_station
aioambient==2023.04.0 aioambient==2023.04.0
@ -367,7 +367,7 @@ beautifulsoup4==4.11.1
bellows==0.35.5 bellows==0.35.5
# homeassistant.components.bmw_connected_drive # homeassistant.components.bmw_connected_drive
bimmer_connected==0.13.6 bimmer-connected==0.13.7
# homeassistant.components.bluetooth # homeassistant.components.bluetooth
bleak-retry-connector==3.0.2 bleak-retry-connector==3.0.2
@ -415,7 +415,7 @@ brottsplatskartan==0.0.1
brunt==1.2.0 brunt==1.2.0
# homeassistant.components.bthome # homeassistant.components.bthome
bthome-ble==2.11.3 bthome-ble==2.12.0
# homeassistant.components.buienradar # homeassistant.components.buienradar
buienradar==1.0.5 buienradar==1.0.5
@ -746,7 +746,7 @@ iaqualink==0.5.0
ibeacon_ble==1.0.1 ibeacon_ble==1.0.1
# homeassistant.components.local_calendar # homeassistant.components.local_calendar
ical==4.5.1 ical==4.5.4
# homeassistant.components.ping # homeassistant.components.ping
icmplib==3.0 icmplib==3.0
@ -1522,7 +1522,7 @@ python-juicenet==1.1.0
python-kasa==0.5.1 python-kasa==0.5.1
# homeassistant.components.matter # homeassistant.components.matter
python-matter-server==3.4.1 python-matter-server==3.5.1
# homeassistant.components.xiaomi_miio # homeassistant.components.xiaomi_miio
python-miio==0.5.12 python-miio==0.5.12
@ -1627,7 +1627,7 @@ radios==0.1.1
radiotherm==2.1.0 radiotherm==2.1.0
# homeassistant.components.rapt_ble # homeassistant.components.rapt_ble
rapt-ble==0.1.1 rapt-ble==0.1.2
# homeassistant.components.rainmachine # homeassistant.components.rainmachine
regenmaschine==2023.06.0 regenmaschine==2023.06.0

View file

@ -858,6 +858,57 @@ async def test_v1_sensors(
}, },
], ],
), ),
(
"A4:C1:38:8D:18:B2",
make_bthome_v2_adv(
"A4:C1:38:8D:18:B2",
b"\x44\x50\x5D\x39\x61\x64",
),
None,
[
{
"sensor_entity": "sensor.test_device_18b2_timestamp",
"friendly_name": "Test Device 18B2 Timestamp",
"unit_of_measurement": "s",
"state_class": "measurement",
"expected_state": "2023-05-14T19:41:17+00:00",
},
],
),
(
"A4:C1:38:8D:18:B2",
make_bthome_v2_adv(
"A4:C1:38:8D:18:B2",
b"\x44\x51\x87\x56",
),
None,
[
{
"sensor_entity": "sensor.test_device_18b2_acceleration",
"friendly_name": "Test Device 18B2 Acceleration",
"unit_of_measurement": "m/s²",
"state_class": "measurement",
"expected_state": "22.151",
},
],
),
(
"A4:C1:38:8D:18:B2",
make_bthome_v2_adv(
"A4:C1:38:8D:18:B2",
b"\x44\x52\x87\x56",
),
None,
[
{
"sensor_entity": "sensor.test_device_18b2_gyroscope",
"friendly_name": "Test Device 18B2 Gyroscope",
"unit_of_measurement": "°/s",
"state_class": "measurement",
"expected_state": "22.151",
},
],
),
( (
"A4:C1:38:8D:18:B2", "A4:C1:38:8D:18:B2",
make_bthome_v2_adv( make_bthome_v2_adv(

View file

@ -1,6 +1,8 @@
"""Test Fully Kiosk Browser services.""" """Test Fully Kiosk Browser services."""
from unittest.mock import MagicMock from unittest.mock import MagicMock
import pytest
from homeassistant.components.fully_kiosk.const import ( from homeassistant.components.fully_kiosk.const import (
ATTR_APPLICATION, ATTR_APPLICATION,
ATTR_URL, ATTR_URL,
@ -10,6 +12,7 @@ from homeassistant.components.fully_kiosk.const import (
) )
from homeassistant.const import ATTR_DEVICE_ID from homeassistant.const import ATTR_DEVICE_ID
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import device_registry as dr from homeassistant.helpers import device_registry as dr
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
@ -28,20 +31,111 @@ async def test_services(
assert device_entry assert device_entry
url = "https://example.com"
await hass.services.async_call( await hass.services.async_call(
DOMAIN, DOMAIN,
SERVICE_LOAD_URL, SERVICE_LOAD_URL,
{ATTR_DEVICE_ID: [device_entry.id], ATTR_URL: "https://example.com"}, {ATTR_DEVICE_ID: [device_entry.id], ATTR_URL: url},
blocking=True, blocking=True,
) )
assert len(mock_fully_kiosk.loadUrl.mock_calls) == 1 mock_fully_kiosk.loadUrl.assert_called_once_with(url)
app = "de.ozerov.fully"
await hass.services.async_call(
DOMAIN,
SERVICE_START_APPLICATION,
{ATTR_DEVICE_ID: [device_entry.id], ATTR_APPLICATION: app},
blocking=True,
)
mock_fully_kiosk.startApplication.assert_called_once_with(app)
async def test_service_unloaded_entry(
hass: HomeAssistant,
mock_fully_kiosk: MagicMock,
init_integration: MockConfigEntry,
) -> None:
"""Test service not called when config entry unloaded."""
await init_integration.async_unload(hass)
device_registry = dr.async_get(hass)
device_entry = device_registry.async_get_device(
identifiers={(DOMAIN, "abcdef-123456")}
)
assert device_entry
with pytest.raises(HomeAssistantError) as excinfo:
await hass.services.async_call(
DOMAIN,
SERVICE_LOAD_URL,
{ATTR_DEVICE_ID: [device_entry.id], ATTR_URL: "https://nabucasa.com"},
blocking=True,
)
assert "Test device is not loaded" in str(excinfo)
mock_fully_kiosk.loadUrl.assert_not_called()
with pytest.raises(HomeAssistantError) as excinfo:
await hass.services.async_call( await hass.services.async_call(
DOMAIN, DOMAIN,
SERVICE_START_APPLICATION, SERVICE_START_APPLICATION,
{ATTR_DEVICE_ID: [device_entry.id], ATTR_APPLICATION: "de.ozerov.fully"}, {ATTR_DEVICE_ID: [device_entry.id], ATTR_APPLICATION: "de.ozerov.fully"},
blocking=True, blocking=True,
) )
assert "Test device is not loaded" in str(excinfo)
mock_fully_kiosk.startApplication.assert_not_called()
assert len(mock_fully_kiosk.startApplication.mock_calls) == 1
async def test_service_bad_device_id(
hass: HomeAssistant,
mock_fully_kiosk: MagicMock,
init_integration: MockConfigEntry,
) -> None:
"""Test Fully Kiosk Browser service invocation with bad device id."""
with pytest.raises(HomeAssistantError) as excinfo:
await hass.services.async_call(
DOMAIN,
SERVICE_LOAD_URL,
{ATTR_DEVICE_ID: ["bad-device_id"], ATTR_URL: "https://example.com"},
blocking=True,
)
assert "Device 'bad-device_id' not found in device registry" in str(excinfo)
async def test_service_called_with_non_fkb_target_devices(
hass: HomeAssistant,
mock_fully_kiosk: MagicMock,
init_integration: MockConfigEntry,
) -> None:
"""Services raise exception when no valid devices provided."""
device_registry = dr.async_get(hass)
other_domain = "NotFullyKiosk"
other_config_id = "555"
await hass.config_entries.async_add(
MockConfigEntry(
title="Not Fully Kiosk", domain=other_domain, entry_id=other_config_id
)
)
device_entry = device_registry.async_get_or_create(
config_entry_id=other_config_id,
identifiers={
(other_domain, 1),
},
)
with pytest.raises(HomeAssistantError) as excinfo:
await hass.services.async_call(
DOMAIN,
SERVICE_LOAD_URL,
{
ATTR_DEVICE_ID: [device_entry.id],
ATTR_URL: "https://example.com",
},
blocking=True,
)
assert f"Device '{device_entry.id}' is not a fully_kiosk device" in str(excinfo)

View file

@ -137,6 +137,40 @@ MOCK_DATA = {
"os_version": "5.15.6-200.fc35.x86_64", "os_version": "5.15.6-200.fc35.x86_64",
"hr_name": "Fedora Linux 35 64bit", "hr_name": "Fedora Linux 35 64bit",
}, },
"raid": {
"md3": {
"status": "active",
"type": "raid1",
"components": {"sdh1": "2", "sdi1": "0"},
"available": "2",
"used": "2",
"config": "UU",
},
"md1": {
"status": "active",
"type": "raid1",
"components": {"sdg": "0", "sde": "1"},
"available": "2",
"used": "2",
"config": "UU",
},
"md4": {
"status": "active",
"type": "raid1",
"components": {"sdf1": "1", "sdb1": "0"},
"available": "2",
"used": "2",
"config": "UU",
},
"md0": {
"status": "active",
"type": "raid1",
"components": {"sdc": "2", "sdd": "3"},
"available": "2",
"used": "2",
"config": "UU",
},
},
"uptime": "3 days, 10:25:20", "uptime": "3 days, 10:25:20",
} }
@ -156,4 +190,22 @@ HA_SENSOR_DATA: dict[str, Any] = {
"memory_free": 2745.0, "memory_free": 2745.0,
}, },
"docker": {"docker_active": 2, "docker_cpu_use": 77.2, "docker_memory_use": 1149.6}, "docker": {"docker_active": 2, "docker_cpu_use": 77.2, "docker_memory_use": 1149.6},
"raid": {
"md3": {
"status": "active",
"type": "raid1",
"components": {"sdh1": "2", "sdi1": "0"},
"available": "2",
"used": "2",
"config": "UU",
},
"md1": {
"status": "active",
"type": "raid1",
"components": {"sdg": "0", "sde": "1"},
"available": "2",
"used": "2",
"config": "UU",
},
},
} }

View file

@ -35,6 +35,14 @@ async def test_sensor_states(hass: HomeAssistant) -> None:
assert state.state == HA_SENSOR_DATA["docker"]["docker_cpu_use"] assert state.state == HA_SENSOR_DATA["docker"]["docker_cpu_use"]
if state := hass.states.get("sensor.0_0_0_0_docker_memory_use"): if state := hass.states.get("sensor.0_0_0_0_docker_memory_use"):
assert state.state == HA_SENSOR_DATA["docker"]["docker_memory_use"] assert state.state == HA_SENSOR_DATA["docker"]["docker_memory_use"]
if state := hass.states.get("sensor.0_0_0_0_md3_available"):
assert state.state == HA_SENSOR_DATA["raid"]["md3"]["available"]
if state := hass.states.get("sensor.0_0_0_0_md3_used"):
assert state.state == HA_SENSOR_DATA["raid"]["md3"]["used"]
if state := hass.states.get("sensor.0_0_0_0_md1_available"):
assert state.state == HA_SENSOR_DATA["raid"]["md1"]["available"]
if state := hass.states.get("sensor.0_0_0_0_md1_used"):
assert state.state == HA_SENSOR_DATA["raid"]["md1"]["used"]
@pytest.mark.parametrize( @pytest.mark.parametrize(

View file

@ -715,3 +715,21 @@ async def test_supervisor_remove_missing_issue_without_error(
msg = await client.receive_json() msg = await client.receive_json()
assert msg["success"] assert msg["success"]
await hass.async_block_till_done() await hass.async_block_till_done()
async def test_system_is_not_ready(
hass: HomeAssistant,
aioclient_mock: AiohttpClientMocker,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Ensure hassio starts despite error."""
aioclient_mock.get(
"http://127.0.0.1/resolution/info",
json={
"result": "",
"message": "System is not ready with state: setup",
},
)
assert await async_setup_component(hass, "hassio", {})
assert "Failed to update supervisor issues" in caplog.text

View file

@ -3,6 +3,7 @@ from __future__ import annotations
import asyncio import asyncio
from unittest.mock import ANY, AsyncMock, MagicMock, Mock, patch from unittest.mock import ANY, AsyncMock, MagicMock, Mock, patch
from uuid import uuid1
from pyhap.accessory import Accessory from pyhap.accessory import Accessory
from pyhap.const import CATEGORY_CAMERA, CATEGORY_TELEVISION from pyhap.const import CATEGORY_CAMERA, CATEGORY_TELEVISION
@ -868,11 +869,11 @@ async def test_homekit_unpair(
homekit.driver.aio_stop_event = MagicMock() homekit.driver.aio_stop_event = MagicMock()
state = homekit.driver.state state = homekit.driver.state
state.add_paired_client("client1", "any", b"1") state.add_paired_client(str(uuid1()).encode("utf-8"), "any", b"1")
state.add_paired_client("client2", "any", b"0") state.add_paired_client(str(uuid1()).encode("utf-8"), "any", b"0")
state.add_paired_client("client3", "any", b"1") state.add_paired_client(str(uuid1()).encode("utf-8"), "any", b"1")
state.add_paired_client("client4", "any", b"0") state.add_paired_client(str(uuid1()).encode("utf-8"), "any", b"0")
state.add_paired_client("client5", "any", b"0") state.add_paired_client(str(uuid1()).encode("utf-8"), "any", b"0")
formatted_mac = dr.format_mac(state.mac) formatted_mac = dr.format_mac(state.mac)
hk_bridge_dev = device_registry.async_get_device( hk_bridge_dev = device_registry.async_get_device(
@ -917,7 +918,8 @@ async def test_homekit_unpair_missing_device_id(
homekit.driver.aio_stop_event = MagicMock() homekit.driver.aio_stop_event = MagicMock()
state = homekit.driver.state state = homekit.driver.state
state.add_paired_client("client1", "any", b"1") client_1 = str(uuid1()).encode("utf-8")
state.add_paired_client(client_1, "any", b"1")
with pytest.raises(HomeAssistantError): with pytest.raises(HomeAssistantError):
await hass.services.async_call( await hass.services.async_call(
DOMAIN, DOMAIN,
@ -926,7 +928,7 @@ async def test_homekit_unpair_missing_device_id(
blocking=True, blocking=True,
) )
await hass.async_block_till_done() await hass.async_block_till_done()
state.paired_clients = {"client1": "any"} state.paired_clients = {client_1.decode("utf-8"): "any"}
homekit.status = STATUS_STOPPED homekit.status = STATUS_STOPPED
@ -967,7 +969,8 @@ async def test_homekit_unpair_not_homekit_device(
) )
state = homekit.driver.state state = homekit.driver.state
state.add_paired_client("client1", "any", b"1") client_1 = str(uuid1()).encode("utf-8")
state.add_paired_client(client_1, "any", b"1")
with pytest.raises(HomeAssistantError): with pytest.raises(HomeAssistantError):
await hass.services.async_call( await hass.services.async_call(
DOMAIN, DOMAIN,
@ -976,7 +979,7 @@ async def test_homekit_unpair_not_homekit_device(
blocking=True, blocking=True,
) )
await hass.async_block_till_done() await hass.async_block_till_done()
state.paired_clients = {"client1": "any"} state.paired_clients = {client_1.decode("utf-8"): "any"}
homekit.status = STATUS_STOPPED homekit.status = STATUS_STOPPED

View file

@ -56,6 +56,7 @@ async def test_if_fires_on_telegram(
identifiers={(DOMAIN, f"_{knx.mock_config_entry.entry_id}_interface")} identifiers={(DOMAIN, f"_{knx.mock_config_entry.entry_id}_interface")}
) )
# "id" field added to action to test if `trigger_data` passed correctly in `async_attach_trigger`
assert await async_setup_component( assert await async_setup_component(
hass, hass,
automation.DOMAIN, automation.DOMAIN,
@ -71,7 +72,8 @@ async def test_if_fires_on_telegram(
"action": { "action": {
"service": "test.automation", "service": "test.automation",
"data_template": { "data_template": {
"catch_all": ("telegram - {{ trigger.destination }}") "catch_all": ("telegram - {{ trigger.destination }}"),
"id": (" {{ trigger.id }}"),
}, },
}, },
}, },
@ -82,11 +84,13 @@ async def test_if_fires_on_telegram(
"device_id": device_entry.id, "device_id": device_entry.id,
"type": "telegram", "type": "telegram",
"destination": ["1/2/3", "1/2/4"], "destination": ["1/2/3", "1/2/4"],
"id": "test-id",
}, },
"action": { "action": {
"service": "test.automation", "service": "test.automation",
"data_template": { "data_template": {
"specific": ("telegram - {{ trigger.destination }}") "specific": ("telegram - {{ trigger.destination }}"),
"id": (" {{ trigger.id }}"),
}, },
}, },
}, },
@ -96,12 +100,18 @@ async def test_if_fires_on_telegram(
await knx.receive_write("0/0/1", (0x03, 0x2F)) await knx.receive_write("0/0/1", (0x03, 0x2F))
assert len(calls) == 1 assert len(calls) == 1
assert calls.pop().data["catch_all"] == "telegram - 0/0/1" test_call = calls.pop()
assert test_call.data["catch_all"] == "telegram - 0/0/1"
assert test_call.data["id"] == 0
await knx.receive_write("1/2/4", (0x03, 0x2F)) await knx.receive_write("1/2/4", (0x03, 0x2F))
assert len(calls) == 2 assert len(calls) == 2
assert calls.pop().data["specific"] == "telegram - 1/2/4" test_call = calls.pop()
assert calls.pop().data["catch_all"] == "telegram - 1/2/4" assert test_call.data["specific"] == "telegram - 1/2/4"
assert test_call.data["id"] == "test-id"
test_call = calls.pop()
assert test_call.data["catch_all"] == "telegram - 1/2/4"
assert test_call.data["id"] == 0
async def test_remove_device_trigger( async def test_remove_device_trigger(

View file

@ -189,14 +189,23 @@ ZONE_5 = {
# 99 is an unknown ZoneType # 99 is an unknown ZoneType
ZONE_6 = { ZONE_6 = {
"ZoneID": "6", "ZoneID": "6",
"ZoneDescription": "Medical", "ZoneDescription": "Unknown",
"ZoneStatus": ZoneStatus.NORMAL, "ZoneStatus": ZoneStatus.NORMAL,
"ZoneTypeId": 99, "ZoneTypeId": 99,
"PartitionId": "1", "PartitionId": "1",
"CanBeBypassed": 0, "CanBeBypassed": 0,
} }
ZONE_INFO = [ZONE_NORMAL, ZONE_2, ZONE_3, ZONE_4, ZONE_5, ZONE_6] ZONE_7 = {
"ZoneID": 7,
"ZoneDescription": "Temperature",
"ZoneStatus": ZoneStatus.NORMAL,
"ZoneTypeId": ZoneType.MONITOR,
"PartitionId": "1",
"CanBeBypassed": 0,
}
ZONE_INFO = [ZONE_NORMAL, ZONE_2, ZONE_3, ZONE_4, ZONE_5, ZONE_6, ZONE_7]
ZONES = {"ZoneInfo": ZONE_INFO} ZONES = {"ZoneInfo": ZONE_INFO}
METADATA_DISARMED = { METADATA_DISARMED = {

View file

@ -84,3 +84,21 @@ async def test_state_and_attributes(hass: HomeAssistant) -> None:
assert state.state == STATE_OFF assert state.state == STATE_OFF
state = hass.states.get("binary_sensor.gas_tamper") state = hass.states.get("binary_sensor.gas_tamper")
assert state.state == STATE_ON assert state.state == STATE_ON
# Zone 6 is unknown type, assume it is a security (door) sensor
state = hass.states.get("binary_sensor.unknown")
assert state.state == STATE_OFF
assert state.attributes.get("device_class") == BinarySensorDeviceClass.DOOR
state = hass.states.get("binary_sensor.unknown_low_battery")
assert state.state == STATE_OFF
state = hass.states.get("binary_sensor.unknown_tamper")
assert state.state == STATE_OFF
# Zone 7 is temperature
state = hass.states.get("binary_sensor.temperature")
assert state.state == STATE_OFF
assert state.attributes.get("device_class") == BinarySensorDeviceClass.PROBLEM
state = hass.states.get("binary_sensor.temperature_low_battery")
assert state.state == STATE_OFF
state = hass.states.get("binary_sensor.temperature_tamper")
assert state.state == STATE_OFF

View file

@ -0,0 +1,42 @@
{
"kind": "youtube#playlistItemListResponse",
"etag": "O0Ah8Wd5pUD2Gsv-n0A42RDRcX8",
"nextPageToken": "EAAaBlBUOkNBVQ",
"items": [
{
"kind": "youtube#playlistItem",
"etag": "qgpoAJRNskzLhD99njC8e2kPB0M",
"id": "VVVfeDVYRzFPVjJQNnVaWjVGU005VHR3Lnd5c3VrRHJNZHFV",
"snippet": {
"publishedAt": "2023-05-11T00:20:46Z",
"channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw",
"title": "What's new in Google Home in less than 1 minute",
"description": "Discover how your connected devices can do more with Google Home using Matter and Automations at Google I/O 2023.\n\nTo learn more about what's new in Google Home, check out the keynote → https://goo.gle/IO23_homekey\n\nSubscribe to Google Developers → https://goo.gle/developers \n\n#GoogleIO #GoogleHome",
"thumbnails": {
"default": {
"url": "https://i.ytimg.com/vi/wysukDrMdqU/default.jpg",
"width": 120,
"height": 90
}
},
"channelTitle": "Google for Developers",
"playlistId": "UU_x5XG1OV2P6uZZ5FSM9Ttw",
"position": 0,
"resourceId": {
"kind": "youtube#video",
"videoId": "wysukDrMdqU"
},
"videoOwnerChannelTitle": "Google for Developers",
"videoOwnerChannelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw"
},
"contentDetails": {
"videoId": "wysukDrMdqU",
"videoPublishedAt": "2023-05-11T00:20:46Z"
}
}
],
"pageInfo": {
"totalResults": 5798,
"resultsPerPage": 1
}
}

View file

@ -0,0 +1,52 @@
{
"kind": "youtube#playlistItemListResponse",
"etag": "O0Ah8Wd5pUD2Gsv-n0A42RDRcX8",
"nextPageToken": "EAAaBlBUOkNBVQ",
"items": [
{
"kind": "youtube#playlistItem",
"etag": "qgpoAJRNskzLhD99njC8e2kPB0M",
"id": "VVVfeDVYRzFPVjJQNnVaWjVGU005VHR3Lnd5c3VrRHJNZHFV",
"snippet": {
"publishedAt": "2023-05-11T00:20:46Z",
"channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw",
"title": "What's new in Google Home in less than 1 minute",
"description": "Discover how your connected devices can do more with Google Home using Matter and Automations at Google I/O 2023.\n\nTo learn more about what's new in Google Home, check out the keynote → https://goo.gle/IO23_homekey\n\nSubscribe to Google Developers → https://goo.gle/developers \n\n#GoogleIO #GoogleHome",
"thumbnails": {
"default": {
"url": "https://i.ytimg.com/vi/wysukDrMdqU/default.jpg",
"width": 120,
"height": 90
},
"medium": {
"url": "https://i.ytimg.com/vi/wysukDrMdqU/mqdefault.jpg",
"width": 320,
"height": 180
},
"high": {
"url": "https://i.ytimg.com/vi/wysukDrMdqU/hqdefault.jpg",
"width": 480,
"height": 360
}
},
"channelTitle": "Google for Developers",
"playlistId": "UU_x5XG1OV2P6uZZ5FSM9Ttw",
"position": 0,
"resourceId": {
"kind": "youtube#video",
"videoId": "wysukDrMdqU"
},
"videoOwnerChannelTitle": "Google for Developers",
"videoOwnerChannelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw"
},
"contentDetails": {
"videoId": "wysukDrMdqU",
"videoPublishedAt": "2023-05-11T00:20:46Z"
}
}
],
"pageInfo": {
"totalResults": 5798,
"resultsPerPage": 1
}
}

View file

@ -0,0 +1,47 @@
{
"kind": "youtube#playlistItemListResponse",
"etag": "O0Ah8Wd5pUD2Gsv-n0A42RDRcX8",
"nextPageToken": "EAAaBlBUOkNBVQ",
"items": [
{
"kind": "youtube#playlistItem",
"etag": "qgpoAJRNskzLhD99njC8e2kPB0M",
"id": "VVVfeDVYRzFPVjJQNnVaWjVGU005VHR3Lnd5c3VrRHJNZHFV",
"snippet": {
"publishedAt": "2023-05-11T00:20:46Z",
"channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw",
"title": "What's new in Google Home in less than 1 minute",
"description": "Discover how your connected devices can do more with Google Home using Matter and Automations at Google I/O 2023.\n\nTo learn more about what's new in Google Home, check out the keynote → https://goo.gle/IO23_homekey\n\nSubscribe to Google Developers → https://goo.gle/developers \n\n#GoogleIO #GoogleHome",
"thumbnails": {
"default": {
"url": "https://i.ytimg.com/vi/wysukDrMdqU/default.jpg",
"width": 120,
"height": 90
},
"medium": {
"url": "https://i.ytimg.com/vi/wysukDrMdqU/mqdefault.jpg",
"width": 320,
"height": 180
}
},
"channelTitle": "Google for Developers",
"playlistId": "UU_x5XG1OV2P6uZZ5FSM9Ttw",
"position": 0,
"resourceId": {
"kind": "youtube#video",
"videoId": "wysukDrMdqU"
},
"videoOwnerChannelTitle": "Google for Developers",
"videoOwnerChannelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw"
},
"contentDetails": {
"videoId": "wysukDrMdqU",
"videoPublishedAt": "2023-05-11T00:20:46Z"
}
}
],
"pageInfo": {
"totalResults": 5798,
"resultsPerPage": 1
}
}

View file

@ -0,0 +1,36 @@
{
"kind": "youtube#playlistItemListResponse",
"etag": "O0Ah8Wd5pUD2Gsv-n0A42RDRcX8",
"nextPageToken": "EAAaBlBUOkNBVQ",
"items": [
{
"kind": "youtube#playlistItem",
"etag": "qgpoAJRNskzLhD99njC8e2kPB0M",
"id": "VVVfeDVYRzFPVjJQNnVaWjVGU005VHR3Lnd5c3VrRHJNZHFV",
"snippet": {
"publishedAt": "2023-05-11T00:20:46Z",
"channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw",
"title": "What's new in Google Home in less than 1 minute",
"description": "Discover how your connected devices can do more with Google Home using Matter and Automations at Google I/O 2023.\n\nTo learn more about what's new in Google Home, check out the keynote → https://goo.gle/IO23_homekey\n\nSubscribe to Google Developers → https://goo.gle/developers \n\n#GoogleIO #GoogleHome",
"thumbnails": {},
"channelTitle": "Google for Developers",
"playlistId": "UU_x5XG1OV2P6uZZ5FSM9Ttw",
"position": 0,
"resourceId": {
"kind": "youtube#video",
"videoId": "wysukDrMdqU"
},
"videoOwnerChannelTitle": "Google for Developers",
"videoOwnerChannelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw"
},
"contentDetails": {
"videoId": "wysukDrMdqU",
"videoPublishedAt": "2023-05-11T00:20:46Z"
}
}
],
"pageInfo": {
"totalResults": 5798,
"resultsPerPage": 1
}
}

View file

@ -0,0 +1,57 @@
{
"kind": "youtube#playlistItemListResponse",
"etag": "O0Ah8Wd5pUD2Gsv-n0A42RDRcX8",
"nextPageToken": "EAAaBlBUOkNBVQ",
"items": [
{
"kind": "youtube#playlistItem",
"etag": "qgpoAJRNskzLhD99njC8e2kPB0M",
"id": "VVVfeDVYRzFPVjJQNnVaWjVGU005VHR3Lnd5c3VrRHJNZHFV",
"snippet": {
"publishedAt": "2023-05-11T00:20:46Z",
"channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw",
"title": "What's new in Google Home in less than 1 minute",
"description": "Discover how your connected devices can do more with Google Home using Matter and Automations at Google I/O 2023.\n\nTo learn more about what's new in Google Home, check out the keynote → https://goo.gle/IO23_homekey\n\nSubscribe to Google Developers → https://goo.gle/developers \n\n#GoogleIO #GoogleHome",
"thumbnails": {
"default": {
"url": "https://i.ytimg.com/vi/wysukDrMdqU/default.jpg",
"width": 120,
"height": 90
},
"medium": {
"url": "https://i.ytimg.com/vi/wysukDrMdqU/mqdefault.jpg",
"width": 320,
"height": 180
},
"high": {
"url": "https://i.ytimg.com/vi/wysukDrMdqU/hqdefault.jpg",
"width": 480,
"height": 360
},
"standard": {
"url": "https://i.ytimg.com/vi/wysukDrMdqU/sddefault.jpg",
"width": 640,
"height": 480
}
},
"channelTitle": "Google for Developers",
"playlistId": "UU_x5XG1OV2P6uZZ5FSM9Ttw",
"position": 0,
"resourceId": {
"kind": "youtube#video",
"videoId": "wysukDrMdqU"
},
"videoOwnerChannelTitle": "Google for Developers",
"videoOwnerChannelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw"
},
"contentDetails": {
"videoId": "wysukDrMdqU",
"videoPublishedAt": "2023-05-11T00:20:46Z"
}
}
],
"pageInfo": {
"totalResults": 5798,
"resultsPerPage": 1
}
}

View file

@ -3,6 +3,7 @@ from datetime import timedelta
from unittest.mock import patch from unittest.mock import patch
from google.auth.exceptions import RefreshError from google.auth.exceptions import RefreshError
import pytest
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.components.youtube import DOMAIN from homeassistant.components.youtube import DOMAIN
@ -87,3 +88,38 @@ async def test_sensor_reauth_trigger(
assert flow["step_id"] == "reauth_confirm" assert flow["step_id"] == "reauth_confirm"
assert flow["handler"] == DOMAIN assert flow["handler"] == DOMAIN
assert flow["context"]["source"] == config_entries.SOURCE_REAUTH assert flow["context"]["source"] == config_entries.SOURCE_REAUTH
@pytest.mark.parametrize(
("fixture", "url", "has_entity_picture"),
[
("standard", "https://i.ytimg.com/vi/wysukDrMdqU/sddefault.jpg", True),
("high", "https://i.ytimg.com/vi/wysukDrMdqU/hqdefault.jpg", True),
("medium", "https://i.ytimg.com/vi/wysukDrMdqU/mqdefault.jpg", True),
("default", "https://i.ytimg.com/vi/wysukDrMdqU/default.jpg", True),
("none", None, False),
],
)
async def test_thumbnail(
hass: HomeAssistant,
setup_integration: ComponentSetup,
fixture: str,
url: str | None,
has_entity_picture: bool,
) -> None:
"""Test if right thumbnail is selected."""
await setup_integration()
with patch(
"homeassistant.components.youtube.api.build",
return_value=MockService(
playlist_items_fixture=f"youtube/thumbnail/{fixture}.json"
),
):
future = dt_util.utcnow() + timedelta(minutes=15)
async_fire_time_changed(hass, future)
await hass.async_block_till_done()
state = hass.states.get("sensor.google_for_developers_latest_upload")
assert state
assert ("entity_picture" in state.attributes) is has_entity_picture
assert state.attributes.get("entity_picture") == url

View file

@ -18,11 +18,11 @@ from homeassistant.components.zwave_js.helpers import (
get_value_id_from_unique_id, get_value_id_from_unique_id,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import async_get as async_get_dev_reg from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.entity_registry import async_get as async_get_ent_reg
from .common import PROPERTY_ULTRAVIOLET from .common import PROPERTY_ULTRAVIOLET
from tests.common import MockConfigEntry
from tests.components.diagnostics import ( from tests.components.diagnostics import (
get_diagnostics_for_config_entry, get_diagnostics_for_config_entry,
get_diagnostics_for_device, get_diagnostics_for_device,
@ -57,10 +57,26 @@ async def test_device_diagnostics(
version_state, version_state,
) -> None: ) -> None:
"""Test the device level diagnostics data dump.""" """Test the device level diagnostics data dump."""
dev_reg = async_get_dev_reg(hass) dev_reg = dr.async_get(hass)
device = dev_reg.async_get_device({get_device_id(client.driver, multisensor_6)}) device = dev_reg.async_get_device({get_device_id(client.driver, multisensor_6)})
assert device assert device
# Create mock config entry for fake entity
mock_config_entry = MockConfigEntry(domain="test_integration")
mock_config_entry.add_to_hass(hass)
# Add an entity entry to the device that is not part of this config entry
ent_reg = er.async_get(hass)
ent_reg.async_get_or_create(
"test",
"test_integration",
"test_unique_id",
suggested_object_id="unrelated_entity",
config_entry=mock_config_entry,
device_id=device.id,
)
assert ent_reg.async_get("test.unrelated_entity")
# Update a value and ensure it is reflected in the node state # Update a value and ensure it is reflected in the node state
event = Event( event = Event(
type="value updated", type="value updated",
@ -92,16 +108,27 @@ async def test_device_diagnostics(
} }
# Assert that we only have the entities that were discovered for this device # Assert that we only have the entities that were discovered for this device
# Entities that are created outside of discovery (e.g. node status sensor and # Entities that are created outside of discovery (e.g. node status sensor and
# ping button) should not be in dump. # ping button) as well as helper entities created from other integrations should
# not be in dump.
assert len(diagnostics_data["entities"]) == len( assert len(diagnostics_data["entities"]) == len(
list(async_discover_node_values(multisensor_6, device, {device.id: set()})) list(async_discover_node_values(multisensor_6, device, {device.id: set()}))
) )
assert any(
entity.entity_id == "test.unrelated_entity"
for entity in er.async_entries_for_device(ent_reg, device.id)
)
# Explicitly check that the entity that is not part of this config entry is not
# in the dump.
assert not any(
entity["entity_id"] == "test.unrelated_entity"
for entity in diagnostics_data["entities"]
)
assert diagnostics_data["state"] == multisensor_6.data assert diagnostics_data["state"] == multisensor_6.data
async def test_device_diagnostics_error(hass: HomeAssistant, integration) -> None: async def test_device_diagnostics_error(hass: HomeAssistant, integration) -> None:
"""Test the device diagnostics raises exception when an invalid device is used.""" """Test the device diagnostics raises exception when an invalid device is used."""
dev_reg = async_get_dev_reg(hass) dev_reg = dr.async_get(hass)
device = dev_reg.async_get_or_create( device = dev_reg.async_get_or_create(
config_entry_id=integration.entry_id, identifiers={("test", "test")} config_entry_id=integration.entry_id, identifiers={("test", "test")}
) )
@ -123,12 +150,12 @@ async def test_device_diagnostics_missing_primary_value(
hass_client: ClientSessionGenerator, hass_client: ClientSessionGenerator,
) -> None: ) -> None:
"""Test that device diagnostics handles an entity with a missing primary value.""" """Test that device diagnostics handles an entity with a missing primary value."""
dev_reg = async_get_dev_reg(hass) dev_reg = dr.async_get(hass)
device = dev_reg.async_get_device({get_device_id(client.driver, multisensor_6)}) device = dev_reg.async_get_device({get_device_id(client.driver, multisensor_6)})
assert device assert device
entity_id = "sensor.multisensor_6_air_temperature" entity_id = "sensor.multisensor_6_air_temperature"
ent_reg = async_get_ent_reg(hass) ent_reg = er.async_get(hass)
entry = ent_reg.async_get(entity_id) entry = ent_reg.async_get(entity_id)
# check that the primary value for the entity exists in the diagnostics # check that the primary value for the entity exists in the diagnostics
@ -212,7 +239,7 @@ async def test_device_diagnostics_secret_value(
client.driver.controller.nodes[node.node_id] = node client.driver.controller.nodes[node.node_id] = node
client.driver.controller.emit("node added", {"node": node}) client.driver.controller.emit("node added", {"node": node})
await hass.async_block_till_done() await hass.async_block_till_done()
dev_reg = async_get_dev_reg(hass) dev_reg = dr.async_get(hass)
device = dev_reg.async_get_device({get_device_id(client.driver, node)}) device = dev_reg.async_get_device({get_device_id(client.driver, node)})
assert device assert device

View file

@ -1112,20 +1112,21 @@ def test_get_trigger_platform_failure() -> None:
async def test_server_reconnect_event( async def test_server_reconnect_event(
hass: HomeAssistant, client, lock_schlage_be469, integration hass: HomeAssistant,
client,
lock_schlage_be469,
lock_schlage_be469_state,
integration,
) -> None: ) -> None:
"""Test that when we reconnect to server, event triggers reattach.""" """Test that when we reconnect to server, event triggers reattach."""
trigger_type = f"{DOMAIN}.event" trigger_type = f"{DOMAIN}.event"
node: Node = lock_schlage_be469 old_node: Node = lock_schlage_be469
dev_reg = async_get_dev_reg(hass)
device = dev_reg.async_get_device(
{get_device_id(client.driver, lock_schlage_be469)}
)
assert device
event_name = "interview stage completed" event_name = "interview stage completed"
original_len = len(node._listeners.get(event_name, [])) old_node = client.driver.controller.nodes[20]
original_len = len(old_node._listeners.get(event_name, []))
assert await async_setup_component( assert await async_setup_component(
hass, hass,
@ -1147,34 +1148,65 @@ async def test_server_reconnect_event(
}, },
) )
assert len(node._listeners.get(event_name, [])) == original_len + 1 assert len(old_node._listeners.get(event_name, [])) == original_len + 1
old_listener = node._listeners.get(event_name, [])[original_len] old_listener = old_node._listeners.get(event_name, [])[original_len]
# Remove node so that we can create a new node instance and make sure the listener
# attaches
node_removed_event = Event(
type="node removed",
data={
"source": "controller",
"event": "node removed",
"replaced": False,
"node": lock_schlage_be469_state,
},
)
client.driver.controller.receive_event(node_removed_event)
assert 20 not in client.driver.controller.nodes
await hass.async_block_till_done()
# Add node like new server connection would
node_added_event = Event(
type="node added",
data={
"source": "controller",
"event": "node added",
"node": lock_schlage_be469_state,
"result": {},
},
)
client.driver.controller.receive_event(node_added_event)
await hass.async_block_till_done()
# Reload integration to trigger the dispatch signal
await hass.config_entries.async_reload(integration.entry_id) await hass.config_entries.async_reload(integration.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
# Make sure there is still a listener added for the trigger # Make sure there is a listener added for the trigger to the new node
assert len(node._listeners.get(event_name, [])) == original_len + 1 new_node = client.driver.controller.nodes[20]
assert len(new_node._listeners.get(event_name, [])) == original_len + 1
# Make sure the old listener was removed # Make sure the old listener is no longer referenced
assert old_listener not in node._listeners.get(event_name, []) assert old_listener not in new_node._listeners.get(event_name, [])
async def test_server_reconnect_value_updated( async def test_server_reconnect_value_updated(
hass: HomeAssistant, client, lock_schlage_be469, integration hass: HomeAssistant,
client,
lock_schlage_be469,
lock_schlage_be469_state,
integration,
) -> None: ) -> None:
"""Test that when we reconnect to server, value_updated triggers reattach.""" """Test that when we reconnect to server, value_updated triggers reattach."""
trigger_type = f"{DOMAIN}.value_updated" trigger_type = f"{DOMAIN}.value_updated"
node: Node = lock_schlage_be469 old_node: Node = lock_schlage_be469
dev_reg = async_get_dev_reg(hass)
device = dev_reg.async_get_device(
{get_device_id(client.driver, lock_schlage_be469)}
)
assert device
event_name = "value updated" event_name = "value updated"
original_len = len(node._listeners.get(event_name, [])) old_node = client.driver.controller.nodes[20]
original_len = len(old_node._listeners.get(event_name, []))
assert await async_setup_component( assert await async_setup_component(
hass, hass,
@ -1196,14 +1228,44 @@ async def test_server_reconnect_value_updated(
}, },
) )
assert len(node._listeners.get(event_name, [])) == original_len + 1 assert len(old_node._listeners.get(event_name, [])) == original_len + 1
old_listener = node._listeners.get(event_name, [])[original_len] old_listener = old_node._listeners.get(event_name, [])[original_len]
# Remove node so that we can create a new node instance and make sure the listener
# attaches
node_removed_event = Event(
type="node removed",
data={
"source": "controller",
"event": "node removed",
"replaced": False,
"node": lock_schlage_be469_state,
},
)
client.driver.controller.receive_event(node_removed_event)
assert 20 not in client.driver.controller.nodes
await hass.async_block_till_done()
# Add node like new server connection would
node_added_event = Event(
type="node added",
data={
"source": "controller",
"event": "node added",
"node": lock_schlage_be469_state,
"result": {},
},
)
client.driver.controller.receive_event(node_added_event)
await hass.async_block_till_done()
# Reload integration to trigger the dispatch signal
await hass.config_entries.async_reload(integration.entry_id) await hass.config_entries.async_reload(integration.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
# Make sure there is still a listener added for the trigger # Make sure there is a listener added for the trigger to the new node
assert len(node._listeners.get(event_name, [])) == original_len + 1 new_node = client.driver.controller.nodes[20]
assert len(new_node._listeners.get(event_name, [])) == original_len + 1
# Make sure the old listener was removed # Make sure the old listener is no longer referenced
assert old_listener not in node._listeners.get(event_name, []) assert old_listener not in new_node._listeners.get(event_name, [])