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",
"iot_class": "local_polling",
"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)
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)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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",
make_bthome_v2_adv(

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

@ -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, [])