2023.6.3 (#95119)
This commit is contained in:
commit
78222bd51c
53 changed files with 898 additions and 223 deletions
|
@ -11,5 +11,5 @@
|
|||
"documentation": "https://www.home-assistant.io/integrations/airzone",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["aioairzone"],
|
||||
"requirements": ["aioairzone==0.6.3"]
|
||||
"requirements": ["aioairzone==0.6.4"]
|
||||
}
|
||||
|
|
|
@ -173,7 +173,11 @@ async def async_setup_scanner(
|
|||
rssi = await hass.async_add_executor_job(client.request_rssi)
|
||||
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:
|
||||
await asyncio.wait(tasks)
|
||||
|
|
|
@ -6,5 +6,5 @@
|
|||
"documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["bimmer_connected"],
|
||||
"requirements": ["bimmer_connected==0.13.6"]
|
||||
"requirements": ["bimmer-connected==0.13.7"]
|
||||
}
|
||||
|
|
|
@ -20,5 +20,5 @@
|
|||
"dependencies": ["bluetooth_adapters"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/bthome",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["bthome-ble==2.11.3"]
|
||||
"requirements": ["bthome-ble==2.12.0"]
|
||||
}
|
||||
|
|
|
@ -47,6 +47,15 @@ from .coordinator import (
|
|||
from .device import device_key_to_bluetooth_entity_key
|
||||
|
||||
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)
|
||||
(BTHomeSensorDeviceClass.BATTERY, Units.PERCENTAGE): SensorEntityDescription(
|
||||
key=f"{BTHomeSensorDeviceClass.BATTERY}_{Units.PERCENTAGE}",
|
||||
|
@ -131,6 +140,15 @@ SENSOR_DESCRIPTIONS = {
|
|||
native_unit_of_measurement=UnitOfVolume.CUBIC_METERS,
|
||||
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)
|
||||
(BTHomeSensorDeviceClass.HUMIDITY, Units.PERCENTAGE): SensorEntityDescription(
|
||||
key=f"{BTHomeSensorDeviceClass.HUMIDITY}_{Units.PERCENTAGE}",
|
||||
|
@ -242,6 +260,15 @@ SENSOR_DESCRIPTIONS = {
|
|||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
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 (-)
|
||||
(
|
||||
BTHomeSensorDeviceClass.UV_INDEX,
|
||||
|
|
|
@ -43,7 +43,7 @@ def get_scanner(hass: HomeAssistant, config: ConfigType) -> FortiOSDeviceScanner
|
|||
fgt = FortiOSAPI()
|
||||
|
||||
try:
|
||||
fgt.tokenlogin(host, token, verify_ssl)
|
||||
fgt.tokenlogin(host, token, verify_ssl, None, 12, "root")
|
||||
except ConnectionError as ex:
|
||||
_LOGGER.error("ConnectionError to FortiOS API: %s", ex)
|
||||
return None
|
||||
|
@ -77,7 +77,12 @@ class FortiOSDeviceScanner(DeviceScanner):
|
|||
|
||||
def update(self):
|
||||
"""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 = []
|
||||
|
@ -85,8 +90,12 @@ class FortiOSDeviceScanner(DeviceScanner):
|
|||
if clients_json:
|
||||
try:
|
||||
for client in clients_json["results"]:
|
||||
if client["is_online"]:
|
||||
self._clients.append(client["mac"].upper())
|
||||
if (
|
||||
"is_online" in client
|
||||
and "master_mac" in client
|
||||
and client["is_online"]
|
||||
):
|
||||
self._clients.append(client["master_mac"].upper())
|
||||
except KeyError as kex:
|
||||
_LOGGER.error("Key not found in clients: %s", kex)
|
||||
|
||||
|
@ -106,17 +115,10 @@ class FortiOSDeviceScanner(DeviceScanner):
|
|||
return None
|
||||
|
||||
for client in data["results"]:
|
||||
if client["mac"] == device:
|
||||
try:
|
||||
if "master_mac" in client and client["master_mac"] == device:
|
||||
if "hostname" in client:
|
||||
name = client["hostname"]
|
||||
_LOGGER.debug("Getting device name=%s", name)
|
||||
return name
|
||||
except KeyError as kex:
|
||||
_LOGGER.debug(
|
||||
"No hostname found for %s in client data: %s",
|
||||
device,
|
||||
kex,
|
||||
)
|
||||
return device.replace(":", "_")
|
||||
|
||||
else:
|
||||
name = client["master_mac"].replace(":", "_")
|
||||
return name
|
||||
return None
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import DOMAIN
|
||||
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:
|
||||
"""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)
|
||||
coordinator.async_update_listeners()
|
||||
|
||||
await async_setup_services(hass)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
|
|
|
@ -1,14 +1,12 @@
|
|||
"""Services for the Fully Kiosk Browser integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
|
||||
from fullykiosk import FullyKiosk
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
|
||||
from homeassistant.const import ATTR_DEVICE_ID
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
import homeassistant.helpers.device_registry as dr
|
||||
|
||||
|
@ -16,59 +14,53 @@ from .const import (
|
|||
ATTR_APPLICATION,
|
||||
ATTR_URL,
|
||||
DOMAIN,
|
||||
LOGGER,
|
||||
SERVICE_LOAD_URL,
|
||||
SERVICE_START_APPLICATION,
|
||||
)
|
||||
from .coordinator import FullyKioskDataUpdateCoordinator
|
||||
|
||||
|
||||
async def async_setup_services(hass: HomeAssistant) -> None:
|
||||
"""Set up the services for the Fully Kiosk Browser integration."""
|
||||
|
||||
async def execute_service(
|
||||
call: ServiceCall,
|
||||
fully_method: Callable,
|
||||
*args: list[str],
|
||||
**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
|
||||
)
|
||||
async def collect_coordinators(
|
||||
device_ids: list[str],
|
||||
) -> list[FullyKioskDataUpdateCoordinator]:
|
||||
config_entries = list[ConfigEntry]()
|
||||
registry = dr.async_get(hass)
|
||||
for target in call.data[ATTR_DEVICE_ID]:
|
||||
for target in device_ids:
|
||||
device = registry.async_get(target)
|
||||
if device:
|
||||
for key in device.config_entries:
|
||||
entry = hass.config_entries.async_get_entry(key)
|
||||
if not entry:
|
||||
continue
|
||||
if entry.domain != DOMAIN:
|
||||
continue
|
||||
coordinator = hass.data[DOMAIN][key]
|
||||
# fully_method(coordinator.fully, *args, **kwargs) would make
|
||||
# test_services.py fail.
|
||||
await getattr(coordinator.fully, fully_method.__name__)(
|
||||
*args, **kwargs
|
||||
device_entries = list[ConfigEntry]()
|
||||
for entry_id in device.config_entries:
|
||||
entry = hass.config_entries.async_get_entry(entry_id)
|
||||
if entry and entry.domain == DOMAIN:
|
||||
device_entries.append(entry)
|
||||
if not device_entries:
|
||||
raise HomeAssistantError(
|
||||
f"Device '{target}' is not a {DOMAIN} device"
|
||||
)
|
||||
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:
|
||||
"""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:
|
||||
"""Start an app on the device."""
|
||||
await execute_service(
|
||||
call, FullyKiosk.startApplication, call.data[ATTR_APPLICATION]
|
||||
)
|
||||
for coordinator in await collect_coordinators(call.data[ATTR_DEVICE_ID]):
|
||||
await coordinator.fully.startApplication(call.data[ATTR_APPLICATION])
|
||||
|
||||
# Register all the above services
|
||||
service_mapping = [
|
||||
|
|
|
@ -223,13 +223,6 @@ SENSOR_TYPES = {
|
|||
icon="mdi:docker",
|
||||
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(
|
||||
key="available",
|
||||
type="raid",
|
||||
|
@ -237,6 +230,13 @@ SENSOR_TYPES = {
|
|||
icon="mdi:harddisk",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
("raid", "used"): GlancesSensorEntityDescription(
|
||||
key="used",
|
||||
type="raid",
|
||||
name_suffix="Raid used",
|
||||
icon="mdi:harddisk",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
|
@ -269,36 +269,36 @@ async def async_setup_entry(
|
|||
if sensor_type in ["fs", "sensors", "raid"]:
|
||||
for sensor_label, params in sensors.items():
|
||||
for param in params:
|
||||
sensor_description = SENSOR_TYPES[(sensor_type, param)]
|
||||
if sensor_description := SENSOR_TYPES.get((sensor_type, param)):
|
||||
_migrate_old_unique_ids(
|
||||
hass,
|
||||
f"{coordinator.host}-{name} {sensor_label} {sensor_description.name_suffix}",
|
||||
f"{sensor_label}-{sensor_description.key}",
|
||||
)
|
||||
entities.append(
|
||||
GlancesSensor(
|
||||
coordinator,
|
||||
name,
|
||||
sensor_label,
|
||||
sensor_description,
|
||||
)
|
||||
)
|
||||
else:
|
||||
for sensor in sensors:
|
||||
if sensor_description := SENSOR_TYPES.get((sensor_type, sensor)):
|
||||
_migrate_old_unique_ids(
|
||||
hass,
|
||||
f"{coordinator.host}-{name} {sensor_label} {sensor_description.name_suffix}",
|
||||
f"{sensor_label}-{sensor_description.key}",
|
||||
f"{coordinator.host}-{name} {sensor_description.name_suffix}",
|
||||
f"-{sensor_description.key}",
|
||||
)
|
||||
entities.append(
|
||||
GlancesSensor(
|
||||
coordinator,
|
||||
name,
|
||||
sensor_label,
|
||||
"",
|
||||
sensor_description,
|
||||
)
|
||||
)
|
||||
else:
|
||||
for sensor in sensors:
|
||||
sensor_description = SENSOR_TYPES[(sensor_type, sensor)]
|
||||
_migrate_old_unique_ids(
|
||||
hass,
|
||||
f"{coordinator.host}-{name} {sensor_description.name_suffix}",
|
||||
f"-{sensor_description.key}",
|
||||
)
|
||||
entities.append(
|
||||
GlancesSensor(
|
||||
coordinator,
|
||||
name,
|
||||
"",
|
||||
sensor_description,
|
||||
)
|
||||
)
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
|
|
|
@ -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.
|
||||
"""
|
||||
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()
|
||||
_LOGGER.debug("Goodwe reset %s to 0", self.name)
|
||||
next_midnight = dt_util.start_of_local_day(
|
||||
|
|
|
@ -24,7 +24,7 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15)
|
|||
SensorType = namedtuple("SensorType", ["name", "icon", "unit", "path"])
|
||||
|
||||
SENSORS_TYPES = {
|
||||
"name": SensorType("Name", None, "", ["profile", "name"]),
|
||||
"name": SensorType("Name", None, None, ["profile", "name"]),
|
||||
"hp": SensorType("HP", "mdi:heart", "HP", ["stats", "hp"]),
|
||||
"maxHealth": SensorType("max HP", "mdi:heart", "HP", ["stats", "maxHealth"]),
|
||||
"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"]
|
||||
),
|
||||
"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 = {
|
||||
|
|
|
@ -305,7 +305,11 @@ class SupervisorIssues:
|
|||
|
||||
async def update(self) -> None:
|
||||
"""Update issues from Supervisor resolution center."""
|
||||
data = await self._client.get_resolution_info()
|
||||
try:
|
||||
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.unsupported_reasons = set(data[ATTR_UNSUPPORTED])
|
||||
|
||||
|
|
|
@ -626,10 +626,10 @@ class HomeDriver(AccessoryDriver): # type: ignore[misc]
|
|||
|
||||
@pyhap_callback # type: ignore[misc]
|
||||
def pair(
|
||||
self, client_uuid: UUID, client_public: str, client_permissions: int
|
||||
self, client_username_bytes: bytes, client_public: str, client_permissions: int
|
||||
) -> bool:
|
||||
"""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:
|
||||
async_dismiss_setup_message(self.hass, self._entry_id)
|
||||
return cast(bool, success)
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
"iot_class": "local_push",
|
||||
"loggers": ["pyhap"],
|
||||
"requirements": [
|
||||
"HAP-python==4.6.0",
|
||||
"HAP-python==4.7.0",
|
||||
"fnv-hash-fast==0.3.1",
|
||||
"PyQRCode==1.2.1",
|
||||
"base36==0.1.1"
|
||||
|
|
|
@ -115,8 +115,8 @@ def add_insteon_events(hass: HomeAssistant, device: Device) -> None:
|
|||
"""Register Insteon device events."""
|
||||
|
||||
@callback
|
||||
def async_fire_group_on_off_event(
|
||||
name: str, address: Address, group: int, button: str
|
||||
def async_fire_insteon_event(
|
||||
name: str, address: Address, group: int, button: str | None = None
|
||||
):
|
||||
# Firing an event when a button is pressed.
|
||||
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():
|
||||
if isinstance(name_or_group, int):
|
||||
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:
|
||||
_register_event(event, async_fire_group_on_off_event)
|
||||
_register_event(event, async_fire_insteon_event)
|
||||
|
||||
|
||||
def register_new_device_callback(hass):
|
||||
|
|
|
@ -84,6 +84,7 @@ async def async_attach_trigger(
|
|||
trigger_info: TriggerInfo,
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Attach a trigger."""
|
||||
trigger_data = trigger_info["trigger_data"]
|
||||
dst_addresses: list[str] = config.get(EXTRA_FIELD_DESTINATION, [])
|
||||
job = HassJob(action, f"KNX device trigger {trigger_info}")
|
||||
knx: KNXModule = hass.data[DOMAIN]
|
||||
|
@ -95,7 +96,7 @@ async def async_attach_trigger(
|
|||
return
|
||||
hass.async_run_hass_job(
|
||||
job,
|
||||
{"trigger": telegram},
|
||||
{"trigger": {**trigger_data, **telegram}},
|
||||
)
|
||||
|
||||
return knx.telegrams.async_listen_telegram(
|
||||
|
|
|
@ -3,7 +3,7 @@ from __future__ import annotations
|
|||
|
||||
import hashlib
|
||||
|
||||
from pylast import LastFMNetwork, Track, User, WSError
|
||||
from pylast import LastFMNetwork, PyLastError, Track, User
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
|
||||
|
@ -104,26 +104,30 @@ class LastFmSensor(SensorEntity):
|
|||
|
||||
def update(self) -> None:
|
||||
"""Update device state."""
|
||||
self._attr_native_value = STATE_NOT_SCROBBLING
|
||||
try:
|
||||
self._user.get_playcount()
|
||||
except WSError as exc:
|
||||
play_count = self._user.get_playcount()
|
||||
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
|
||||
LOGGER.error("Failed to load LastFM user `%s`: %r", self._user.name, exc)
|
||||
return
|
||||
self._attr_entity_picture = self._user.get_image()
|
||||
if now_playing := self._user.get_now_playing():
|
||||
self._attr_available = True
|
||||
if 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 = {
|
||||
ATTR_LAST_PLAYED: last_played,
|
||||
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
|
||||
)
|
||||
|
|
|
@ -6,5 +6,5 @@
|
|||
"documentation": "https://www.home-assistant.io/integrations/local_calendar",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["ical"],
|
||||
"requirements": ["ical==4.5.1"]
|
||||
"requirements": ["ical==4.5.4"]
|
||||
}
|
||||
|
|
|
@ -2,11 +2,12 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from contextlib import suppress
|
||||
|
||||
import async_timeout
|
||||
from matter_server.client import MatterClient
|
||||
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
|
||||
|
||||
from homeassistant.components.hassio import AddonError, AddonManager, AddonState
|
||||
|
@ -207,7 +208,9 @@ async def async_remove_config_entry_device(
|
|||
)
|
||||
|
||||
matter = get_matter(hass)
|
||||
await matter.matter_client.remove_node(node.node_id)
|
||||
with suppress(NodeNotExists):
|
||||
# ignore if the server has already removed the node.
|
||||
await matter.matter_client.remove_node(node.node_id)
|
||||
|
||||
return True
|
||||
|
||||
|
|
|
@ -92,7 +92,7 @@ class MatterAdapter:
|
|||
get_clean_name(basic_info.nodeLabel)
|
||||
or get_clean_name(basic_info.productLabel)
|
||||
or get_clean_name(basic_info.productName)
|
||||
or device_type.__class__.__name__
|
||||
or device_type.__name__
|
||||
if device_type
|
||||
else None
|
||||
)
|
||||
|
@ -117,7 +117,7 @@ class MatterAdapter:
|
|||
identifiers.add((DOMAIN, f"{ID_TYPE_SERIAL}_{basic_info.serialNumber}"))
|
||||
|
||||
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
|
||||
else None
|
||||
)
|
||||
|
|
|
@ -6,5 +6,5 @@
|
|||
"dependencies": ["websocket_api"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/matter",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["python-matter-server==3.4.1"]
|
||||
"requirements": ["python-matter-server==3.5.1"]
|
||||
}
|
||||
|
|
|
@ -133,10 +133,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||
|
||||
await coordinator_alert.async_refresh()
|
||||
|
||||
if not coordinator_alert.last_update_success:
|
||||
raise ConfigEntryNotReady
|
||||
|
||||
hass.data[DOMAIN][department] = True
|
||||
if coordinator_alert.last_update_success:
|
||||
hass.data[DOMAIN][department] = True
|
||||
else:
|
||||
_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)
|
||||
|
||||
hass.data[DOMAIN][entry.entry_id] = {
|
||||
UNDO_UPDATE_LISTENER: undo_listener,
|
||||
COORDINATOR_FORECAST: coordinator_forecast,
|
||||
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)
|
||||
|
||||
|
|
|
@ -16,5 +16,5 @@
|
|||
"dependencies": ["bluetooth_adapters"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/rapt_ble",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["rapt-ble==0.1.1"]
|
||||
"requirements": ["rapt-ble==0.1.2"]
|
||||
}
|
||||
|
|
|
@ -81,15 +81,9 @@ class TotalConnectZoneSecurityBinarySensor(TotalConnectZoneBinarySensor):
|
|||
return BinarySensorDeviceClass.MOTION
|
||||
if self._zone.is_type_medical():
|
||||
return BinarySensorDeviceClass.SAFETY
|
||||
# "security" type is a generic category so test for it last
|
||||
if self._zone.is_type_security():
|
||||
return BinarySensorDeviceClass.DOOR
|
||||
|
||||
_LOGGER.error(
|
||||
"TotalConnect zone %s reported an unexpected device class",
|
||||
self._zone.zoneid,
|
||||
)
|
||||
return None
|
||||
if self._zone.is_type_temperature():
|
||||
return BinarySensorDeviceClass.PROBLEM
|
||||
return BinarySensorDeviceClass.DOOR
|
||||
|
||||
def update(self):
|
||||
"""Return the state of the device."""
|
||||
|
|
|
@ -85,9 +85,16 @@ class YouTubeDataUpdateCoordinator(DataUpdateCoordinator):
|
|||
ATTR_PUBLISHED_AT: video["snippet"]["publishedAt"],
|
||||
ATTR_TITLE: video["snippet"]["title"],
|
||||
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_SUBSCRIBER_COUNT: int(channel["statistics"]["subscriberCount"]),
|
||||
}
|
||||
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
|
||||
|
|
|
@ -30,7 +30,7 @@ class YouTubeMixin:
|
|||
"""Mixin for required keys."""
|
||||
|
||||
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
|
||||
|
||||
|
||||
|
@ -87,7 +87,7 @@ class YouTubeSensor(YouTubeChannelEntity, SensorEntity):
|
|||
return self.entity_description.value_fn(self.coordinator.data[self._channel_id])
|
||||
|
||||
@property
|
||||
def entity_picture(self) -> str:
|
||||
def entity_picture(self) -> str | None:
|
||||
"""Return the value reported by the sensor."""
|
||||
return self.entity_description.entity_picture_fn(
|
||||
self.coordinator.data[self._channel_id]
|
||||
|
|
|
@ -17,6 +17,10 @@
|
|||
"data": {
|
||||
"channels": "YouTube channels"
|
||||
}
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"title": "[%key:common::config_flow::title::reauth%]",
|
||||
"description": "The YouTube integration needs to re-authenticate your account"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -65,7 +65,7 @@ def redact_node_state(node_state: NodeDataType) -> NodeDataType:
|
|||
|
||||
|
||||
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]]:
|
||||
"""Get entities for a device."""
|
||||
entity_entries = er.async_entries_for_device(
|
||||
|
@ -73,6 +73,10 @@ def get_device_entities(
|
|||
)
|
||||
entities = []
|
||||
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 (value_id := get_value_id_from_unique_id(entry.unique_id)) is None:
|
||||
continue
|
||||
|
@ -142,7 +146,7 @@ async def async_get_device_diagnostics(
|
|||
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")
|
||||
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
|
||||
node_state = redact_node_state(async_redact_data(node.data, KEYS_TO_REDACT))
|
||||
return {
|
||||
|
|
|
@ -142,8 +142,9 @@ async def async_attach_trigger(
|
|||
) -> CALLBACK_TYPE:
|
||||
"""Listen for state changes based on configuration."""
|
||||
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 nodes:
|
||||
if config[ATTR_EVENT_SOURCE] == "node" and not async_get_nodes_from_targets(
|
||||
hass, config, dev_reg=dev_reg
|
||||
):
|
||||
raise ValueError(
|
||||
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
|
||||
# server connections for all of them.
|
||||
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]
|
||||
client: Client = hass.data[DOMAIN][entry_id][DATA_CLIENT]
|
||||
driver = client.driver
|
||||
|
|
|
@ -91,7 +91,7 @@ async def async_attach_trigger(
|
|||
) -> CALLBACK_TYPE:
|
||||
"""Listen for state changes based on configuration."""
|
||||
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(
|
||||
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
|
||||
# server connections for all of them.
|
||||
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
|
||||
assert driver is not None # The node comes from the driver.
|
||||
drivers.add(driver)
|
||||
|
|
|
@ -8,7 +8,7 @@ from .backports.enum import StrEnum
|
|||
APPLICATION_NAME: Final = "HomeAssistant"
|
||||
MAJOR_VERSION: Final = 2023
|
||||
MINOR_VERSION: Final = 6
|
||||
PATCH_VERSION: Final = "2"
|
||||
PATCH_VERSION: Final = "3"
|
||||
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
||||
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 10, 0)
|
||||
|
|
|
@ -46,7 +46,7 @@ pyyaml==6.0
|
|||
requests==2.31.0
|
||||
scapy==2.5.0
|
||||
sqlalchemy==2.0.15
|
||||
typing-extensions>=4.5.0,<5.0
|
||||
typing_extensions>=4.6.3,<5.0
|
||||
ulid-transform==0.7.2
|
||||
voluptuous-serialize==2.6.0
|
||||
voluptuous==0.13.1
|
||||
|
|
|
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||
|
||||
[project]
|
||||
name = "homeassistant"
|
||||
version = "2023.6.2"
|
||||
version = "2023.6.3"
|
||||
license = {text = "Apache-2.0"}
|
||||
description = "Open-source home automation platform running on Python 3."
|
||||
readme = "README.rst"
|
||||
|
@ -49,7 +49,7 @@ dependencies = [
|
|||
"python-slugify==4.0.1",
|
||||
"pyyaml==6.0",
|
||||
"requests==2.31.0",
|
||||
"typing-extensions>=4.5.0,<5.0",
|
||||
"typing_extensions>=4.6.3,<5.0",
|
||||
"ulid-transform==0.7.2",
|
||||
"voluptuous==0.13.1",
|
||||
"voluptuous-serialize==2.6.0",
|
||||
|
|
|
@ -23,7 +23,7 @@ pip>=21.0,<23.2
|
|||
python-slugify==4.0.1
|
||||
pyyaml==6.0
|
||||
requests==2.31.0
|
||||
typing-extensions>=4.5.0,<5.0
|
||||
typing_extensions>=4.6.3,<5.0
|
||||
ulid-transform==0.7.2
|
||||
voluptuous==0.13.1
|
||||
voluptuous-serialize==2.6.0
|
||||
|
|
|
@ -11,7 +11,7 @@ AIOAladdinConnect==0.1.56
|
|||
Adax-local==0.1.5
|
||||
|
||||
# homeassistant.components.homekit
|
||||
HAP-python==4.6.0
|
||||
HAP-python==4.7.0
|
||||
|
||||
# homeassistant.components.mastodon
|
||||
Mastodon.py==1.5.1
|
||||
|
@ -119,7 +119,7 @@ aioairq==0.2.4
|
|||
aioairzone-cloud==0.1.8
|
||||
|
||||
# homeassistant.components.airzone
|
||||
aioairzone==0.6.3
|
||||
aioairzone==0.6.4
|
||||
|
||||
# homeassistant.components.ambient_station
|
||||
aioambient==2023.04.0
|
||||
|
@ -434,7 +434,7 @@ beautifulsoup4==4.11.1
|
|||
bellows==0.35.5
|
||||
|
||||
# homeassistant.components.bmw_connected_drive
|
||||
bimmer_connected==0.13.6
|
||||
bimmer-connected==0.13.7
|
||||
|
||||
# homeassistant.components.bizkaibus
|
||||
bizkaibus==0.1.1
|
||||
|
@ -502,7 +502,7 @@ brunt==1.2.0
|
|||
bt_proximity==0.2.1
|
||||
|
||||
# homeassistant.components.bthome
|
||||
bthome-ble==2.11.3
|
||||
bthome-ble==2.12.0
|
||||
|
||||
# homeassistant.components.bt_home_hub_5
|
||||
bthomehub5-devicelist==0.1.1
|
||||
|
@ -966,7 +966,7 @@ ibeacon_ble==1.0.1
|
|||
ibmiotf==0.3.4
|
||||
|
||||
# homeassistant.components.local_calendar
|
||||
ical==4.5.1
|
||||
ical==4.5.4
|
||||
|
||||
# homeassistant.components.ping
|
||||
icmplib==3.0
|
||||
|
@ -2087,7 +2087,7 @@ python-kasa==0.5.1
|
|||
# python-lirc==1.2.3
|
||||
|
||||
# homeassistant.components.matter
|
||||
python-matter-server==3.4.1
|
||||
python-matter-server==3.5.1
|
||||
|
||||
# homeassistant.components.xiaomi_miio
|
||||
python-miio==0.5.12
|
||||
|
@ -2240,7 +2240,7 @@ radiotherm==2.1.0
|
|||
raincloudy==0.0.7
|
||||
|
||||
# homeassistant.components.rapt_ble
|
||||
rapt-ble==0.1.1
|
||||
rapt-ble==0.1.2
|
||||
|
||||
# homeassistant.components.raspyrfm
|
||||
raspyrfm-client==1.2.8
|
||||
|
|
|
@ -13,7 +13,7 @@ AIOAladdinConnect==0.1.56
|
|||
Adax-local==0.1.5
|
||||
|
||||
# homeassistant.components.homekit
|
||||
HAP-python==4.6.0
|
||||
HAP-python==4.7.0
|
||||
|
||||
# homeassistant.components.flick_electric
|
||||
PyFlick==0.0.2
|
||||
|
@ -109,7 +109,7 @@ aioairq==0.2.4
|
|||
aioairzone-cloud==0.1.8
|
||||
|
||||
# homeassistant.components.airzone
|
||||
aioairzone==0.6.3
|
||||
aioairzone==0.6.4
|
||||
|
||||
# homeassistant.components.ambient_station
|
||||
aioambient==2023.04.0
|
||||
|
@ -367,7 +367,7 @@ beautifulsoup4==4.11.1
|
|||
bellows==0.35.5
|
||||
|
||||
# homeassistant.components.bmw_connected_drive
|
||||
bimmer_connected==0.13.6
|
||||
bimmer-connected==0.13.7
|
||||
|
||||
# homeassistant.components.bluetooth
|
||||
bleak-retry-connector==3.0.2
|
||||
|
@ -415,7 +415,7 @@ brottsplatskartan==0.0.1
|
|||
brunt==1.2.0
|
||||
|
||||
# homeassistant.components.bthome
|
||||
bthome-ble==2.11.3
|
||||
bthome-ble==2.12.0
|
||||
|
||||
# homeassistant.components.buienradar
|
||||
buienradar==1.0.5
|
||||
|
@ -746,7 +746,7 @@ iaqualink==0.5.0
|
|||
ibeacon_ble==1.0.1
|
||||
|
||||
# homeassistant.components.local_calendar
|
||||
ical==4.5.1
|
||||
ical==4.5.4
|
||||
|
||||
# homeassistant.components.ping
|
||||
icmplib==3.0
|
||||
|
@ -1522,7 +1522,7 @@ python-juicenet==1.1.0
|
|||
python-kasa==0.5.1
|
||||
|
||||
# homeassistant.components.matter
|
||||
python-matter-server==3.4.1
|
||||
python-matter-server==3.5.1
|
||||
|
||||
# homeassistant.components.xiaomi_miio
|
||||
python-miio==0.5.12
|
||||
|
@ -1627,7 +1627,7 @@ radios==0.1.1
|
|||
radiotherm==2.1.0
|
||||
|
||||
# homeassistant.components.rapt_ble
|
||||
rapt-ble==0.1.1
|
||||
rapt-ble==0.1.2
|
||||
|
||||
# homeassistant.components.rainmachine
|
||||
regenmaschine==2023.06.0
|
||||
|
|
|
@ -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",
|
||||
make_bthome_v2_adv(
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
"""Test Fully Kiosk Browser services."""
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.fully_kiosk.const import (
|
||||
ATTR_APPLICATION,
|
||||
ATTR_URL,
|
||||
|
@ -10,6 +12,7 @@ from homeassistant.components.fully_kiosk.const import (
|
|||
)
|
||||
from homeassistant.const import ATTR_DEVICE_ID
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
@ -28,20 +31,111 @@ async def test_services(
|
|||
|
||||
assert device_entry
|
||||
|
||||
url = "https://example.com"
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
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,
|
||||
)
|
||||
|
||||
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: "de.ozerov.fully"},
|
||||
{ATTR_DEVICE_ID: [device_entry.id], ATTR_APPLICATION: app},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert len(mock_fully_kiosk.startApplication.mock_calls) == 1
|
||||
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(
|
||||
DOMAIN,
|
||||
SERVICE_START_APPLICATION,
|
||||
{ATTR_DEVICE_ID: [device_entry.id], ATTR_APPLICATION: "de.ozerov.fully"},
|
||||
blocking=True,
|
||||
)
|
||||
assert "Test device is not loaded" in str(excinfo)
|
||||
mock_fully_kiosk.startApplication.assert_not_called()
|
||||
|
||||
|
||||
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)
|
||||
|
|
|
@ -137,6 +137,40 @@ MOCK_DATA = {
|
|||
"os_version": "5.15.6-200.fc35.x86_64",
|
||||
"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",
|
||||
}
|
||||
|
||||
|
@ -156,4 +190,22 @@ HA_SENSOR_DATA: dict[str, Any] = {
|
|||
"memory_free": 2745.0,
|
||||
},
|
||||
"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",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -35,6 +35,14 @@ async def test_sensor_states(hass: HomeAssistant) -> None:
|
|||
assert state.state == HA_SENSOR_DATA["docker"]["docker_cpu_use"]
|
||||
if state := hass.states.get("sensor.0_0_0_0_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(
|
||||
|
|
|
@ -715,3 +715,21 @@ async def test_supervisor_remove_missing_issue_without_error(
|
|||
msg = await client.receive_json()
|
||||
assert msg["success"]
|
||||
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
|
||||
|
|
|
@ -3,6 +3,7 @@ from __future__ import annotations
|
|||
|
||||
import asyncio
|
||||
from unittest.mock import ANY, AsyncMock, MagicMock, Mock, patch
|
||||
from uuid import uuid1
|
||||
|
||||
from pyhap.accessory import Accessory
|
||||
from pyhap.const import CATEGORY_CAMERA, CATEGORY_TELEVISION
|
||||
|
@ -868,11 +869,11 @@ async def test_homekit_unpair(
|
|||
homekit.driver.aio_stop_event = MagicMock()
|
||||
|
||||
state = homekit.driver.state
|
||||
state.add_paired_client("client1", "any", b"1")
|
||||
state.add_paired_client("client2", "any", b"0")
|
||||
state.add_paired_client("client3", "any", b"1")
|
||||
state.add_paired_client("client4", "any", b"0")
|
||||
state.add_paired_client("client5", "any", b"0")
|
||||
state.add_paired_client(str(uuid1()).encode("utf-8"), "any", b"1")
|
||||
state.add_paired_client(str(uuid1()).encode("utf-8"), "any", b"0")
|
||||
state.add_paired_client(str(uuid1()).encode("utf-8"), "any", b"1")
|
||||
state.add_paired_client(str(uuid1()).encode("utf-8"), "any", b"0")
|
||||
state.add_paired_client(str(uuid1()).encode("utf-8"), "any", b"0")
|
||||
|
||||
formatted_mac = dr.format_mac(state.mac)
|
||||
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()
|
||||
|
||||
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):
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
|
@ -926,7 +928,7 @@ async def test_homekit_unpair_missing_device_id(
|
|||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
state.paired_clients = {"client1": "any"}
|
||||
state.paired_clients = {client_1.decode("utf-8"): "any"}
|
||||
homekit.status = STATUS_STOPPED
|
||||
|
||||
|
||||
|
@ -967,7 +969,8 @@ async def test_homekit_unpair_not_homekit_device(
|
|||
)
|
||||
|
||||
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):
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
|
@ -976,7 +979,7 @@ async def test_homekit_unpair_not_homekit_device(
|
|||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
state.paired_clients = {"client1": "any"}
|
||||
state.paired_clients = {client_1.decode("utf-8"): "any"}
|
||||
homekit.status = STATUS_STOPPED
|
||||
|
||||
|
||||
|
|
|
@ -56,6 +56,7 @@ async def test_if_fires_on_telegram(
|
|||
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(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
|
@ -71,7 +72,8 @@ async def test_if_fires_on_telegram(
|
|||
"action": {
|
||||
"service": "test.automation",
|
||||
"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,
|
||||
"type": "telegram",
|
||||
"destination": ["1/2/3", "1/2/4"],
|
||||
"id": "test-id",
|
||||
},
|
||||
"action": {
|
||||
"service": "test.automation",
|
||||
"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))
|
||||
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))
|
||||
assert len(calls) == 2
|
||||
assert calls.pop().data["specific"] == "telegram - 1/2/4"
|
||||
assert calls.pop().data["catch_all"] == "telegram - 1/2/4"
|
||||
test_call = calls.pop()
|
||||
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(
|
||||
|
|
|
@ -189,14 +189,23 @@ ZONE_5 = {
|
|||
# 99 is an unknown ZoneType
|
||||
ZONE_6 = {
|
||||
"ZoneID": "6",
|
||||
"ZoneDescription": "Medical",
|
||||
"ZoneDescription": "Unknown",
|
||||
"ZoneStatus": ZoneStatus.NORMAL,
|
||||
"ZoneTypeId": 99,
|
||||
"PartitionId": "1",
|
||||
"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}
|
||||
|
||||
METADATA_DISARMED = {
|
||||
|
|
|
@ -84,3 +84,21 @@ async def test_state_and_attributes(hass: HomeAssistant) -> None:
|
|||
assert state.state == STATE_OFF
|
||||
state = hass.states.get("binary_sensor.gas_tamper")
|
||||
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
|
||||
|
|
42
tests/components/youtube/fixtures/thumbnail/default.json
Normal file
42
tests/components/youtube/fixtures/thumbnail/default.json
Normal 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
|
||||
}
|
||||
}
|
52
tests/components/youtube/fixtures/thumbnail/high.json
Normal file
52
tests/components/youtube/fixtures/thumbnail/high.json
Normal 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
|
||||
}
|
||||
}
|
47
tests/components/youtube/fixtures/thumbnail/medium.json
Normal file
47
tests/components/youtube/fixtures/thumbnail/medium.json
Normal 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
|
||||
}
|
||||
}
|
36
tests/components/youtube/fixtures/thumbnail/none.json
Normal file
36
tests/components/youtube/fixtures/thumbnail/none.json
Normal 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
|
||||
}
|
||||
}
|
57
tests/components/youtube/fixtures/thumbnail/standard.json
Normal file
57
tests/components/youtube/fixtures/thumbnail/standard.json
Normal 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
|
||||
}
|
||||
}
|
|
@ -3,6 +3,7 @@ from datetime import timedelta
|
|||
from unittest.mock import patch
|
||||
|
||||
from google.auth.exceptions import RefreshError
|
||||
import pytest
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.youtube import DOMAIN
|
||||
|
@ -87,3 +88,38 @@ async def test_sensor_reauth_trigger(
|
|||
assert flow["step_id"] == "reauth_confirm"
|
||||
assert flow["handler"] == DOMAIN
|
||||
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
|
||||
|
|
|
@ -18,11 +18,11 @@ from homeassistant.components.zwave_js.helpers import (
|
|||
get_value_id_from_unique_id,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import async_get as async_get_dev_reg
|
||||
from homeassistant.helpers.entity_registry import async_get as async_get_ent_reg
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
|
||||
from .common import PROPERTY_ULTRAVIOLET
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.components.diagnostics import (
|
||||
get_diagnostics_for_config_entry,
|
||||
get_diagnostics_for_device,
|
||||
|
@ -57,10 +57,26 @@ async def test_device_diagnostics(
|
|||
version_state,
|
||||
) -> None:
|
||||
"""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)})
|
||||
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
|
||||
event = Event(
|
||||
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
|
||||
# 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(
|
||||
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
|
||||
|
||||
|
||||
async def test_device_diagnostics_error(hass: HomeAssistant, integration) -> None:
|
||||
"""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(
|
||||
config_entry_id=integration.entry_id, identifiers={("test", "test")}
|
||||
)
|
||||
|
@ -123,12 +150,12 @@ async def test_device_diagnostics_missing_primary_value(
|
|||
hass_client: ClientSessionGenerator,
|
||||
) -> None:
|
||||
"""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)})
|
||||
assert device
|
||||
|
||||
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)
|
||||
|
||||
# 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.emit("node added", {"node": node})
|
||||
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)})
|
||||
assert device
|
||||
|
||||
|
|
|
@ -1112,20 +1112,21 @@ def test_get_trigger_platform_failure() -> None:
|
|||
|
||||
|
||||
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:
|
||||
"""Test that when we reconnect to server, event triggers reattach."""
|
||||
trigger_type = f"{DOMAIN}.event"
|
||||
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
|
||||
old_node: Node = lock_schlage_be469
|
||||
|
||||
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(
|
||||
hass,
|
||||
|
@ -1147,34 +1148,65 @@ async def test_server_reconnect_event(
|
|||
},
|
||||
)
|
||||
|
||||
assert len(node._listeners.get(event_name, [])) == original_len + 1
|
||||
old_listener = node._listeners.get(event_name, [])[original_len]
|
||||
assert len(old_node._listeners.get(event_name, [])) == original_len + 1
|
||||
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.async_block_till_done()
|
||||
|
||||
# Make sure there is still a listener added for the trigger
|
||||
assert len(node._listeners.get(event_name, [])) == original_len + 1
|
||||
# Make sure there is a listener added for the trigger to the new node
|
||||
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
|
||||
assert old_listener not in node._listeners.get(event_name, [])
|
||||
# Make sure the old listener is no longer referenced
|
||||
assert old_listener not in new_node._listeners.get(event_name, [])
|
||||
|
||||
|
||||
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:
|
||||
"""Test that when we reconnect to server, value_updated triggers reattach."""
|
||||
trigger_type = f"{DOMAIN}.value_updated"
|
||||
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
|
||||
old_node: Node = lock_schlage_be469
|
||||
|
||||
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(
|
||||
hass,
|
||||
|
@ -1196,14 +1228,44 @@ async def test_server_reconnect_value_updated(
|
|||
},
|
||||
)
|
||||
|
||||
assert len(node._listeners.get(event_name, [])) == original_len + 1
|
||||
old_listener = node._listeners.get(event_name, [])[original_len]
|
||||
assert len(old_node._listeners.get(event_name, [])) == original_len + 1
|
||||
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.async_block_till_done()
|
||||
|
||||
# Make sure there is still a listener added for the trigger
|
||||
assert len(node._listeners.get(event_name, [])) == original_len + 1
|
||||
# Make sure there is a listener added for the trigger to the new node
|
||||
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
|
||||
assert old_listener not in node._listeners.get(event_name, [])
|
||||
# Make sure the old listener is no longer referenced
|
||||
assert old_listener not in new_node._listeners.get(event_name, [])
|
||||
|
|
Loading…
Add table
Reference in a new issue