mirror of
https://github.com/home-assistant/core.git
synced 2026-01-05 23:35:24 +01:00
Compare commits
40 Commits
2022.2.0b0
...
2022.2.0b2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8e38b7624e | ||
|
|
fdb52df7b7 | ||
|
|
6c3e8b06ea | ||
|
|
6ba52b1c86 | ||
|
|
1e60958fc4 | ||
|
|
0f9e65e687 | ||
|
|
d382e24e5b | ||
|
|
82acaa380c | ||
|
|
0a00177a8f | ||
|
|
34cf82b017 | ||
|
|
44403dab62 | ||
|
|
909b0fb689 | ||
|
|
3f763ddc9a | ||
|
|
837d49f67b | ||
|
|
735edd83fc | ||
|
|
7415513352 | ||
|
|
6f20a75583 | ||
|
|
05d7fef9f0 | ||
|
|
2ff8f10b9f | ||
|
|
0604185854 | ||
|
|
ff445b69f4 | ||
|
|
7e2d04ca77 | ||
|
|
07d2627dc5 | ||
|
|
1968ddb3fd | ||
|
|
5a90f106d1 | ||
|
|
8afb0aa44a | ||
|
|
6f8b0a01b4 | ||
|
|
035b589fca | ||
|
|
3e94d39c64 | ||
|
|
a768de51c0 | ||
|
|
25ffda7cd4 | ||
|
|
290a0df2be | ||
|
|
662ec1377a | ||
|
|
057f1a701f | ||
|
|
03e369dc86 | ||
|
|
c831270262 | ||
|
|
9eb18564b7 | ||
|
|
a7d83993be | ||
|
|
25ea728f21 | ||
|
|
63048a67e0 |
@@ -2,7 +2,7 @@
|
||||
"domain": "arris_tg2492lg",
|
||||
"name": "Arris TG2492LG",
|
||||
"documentation": "https://www.home-assistant.io/integrations/arris_tg2492lg",
|
||||
"requirements": ["arris-tg2492lg==1.1.0"],
|
||||
"requirements": ["arris-tg2492lg==1.2.1"],
|
||||
"codeowners": ["@vanbalken"],
|
||||
"iot_class": "local_polling"
|
||||
}
|
||||
|
||||
@@ -34,7 +34,9 @@ variables:
|
||||
|
||||
condition:
|
||||
condition: template
|
||||
value_template: "{{ trigger.from_state.state == zone_state and trigger.to_state.state != zone_state }}"
|
||||
# The first case handles leaving the Home zone which has a special state when zoning called 'home'.
|
||||
# The second case handles leaving all other zones.
|
||||
value_template: "{{ zone_entity == 'zone.home' and trigger.from_state.state == 'home' and trigger.to_state.state != 'home' or trigger.from_state.state == zone_state and trigger.to_state.state != zone_state }}"
|
||||
|
||||
action:
|
||||
- alias: "Notify that a person has left the zone"
|
||||
|
||||
@@ -471,9 +471,16 @@ class CastDevice(MediaPlayerEntity):
|
||||
"audio/"
|
||||
)
|
||||
|
||||
if plex.is_plex_media_id(media_content_id):
|
||||
return await plex.async_browse_media(
|
||||
self.hass, media_content_type, media_content_id, platform=CAST_DOMAIN
|
||||
if media_content_id is not None:
|
||||
if plex.is_plex_media_id(media_content_id):
|
||||
return await plex.async_browse_media(
|
||||
self.hass,
|
||||
media_content_type,
|
||||
media_content_id,
|
||||
platform=CAST_DOMAIN,
|
||||
)
|
||||
return await media_source.async_browse_media(
|
||||
self.hass, media_content_id, **kwargs
|
||||
)
|
||||
|
||||
if media_content_type == "plex":
|
||||
|
||||
@@ -192,10 +192,10 @@ class CloudAlexaConfig(alexa_config.AbstractConfig):
|
||||
|
||||
if self.should_report_state != self.is_reporting_states:
|
||||
if self.should_report_state:
|
||||
with suppress(
|
||||
alexa_errors.NoTokenAvailable, alexa_errors.RequireRelink
|
||||
):
|
||||
try:
|
||||
await self.async_enable_proactive_mode()
|
||||
except (alexa_errors.NoTokenAvailable, alexa_errors.RequireRelink):
|
||||
await self.set_authorized(False)
|
||||
else:
|
||||
await self.async_disable_proactive_mode()
|
||||
|
||||
|
||||
@@ -224,6 +224,7 @@ class TrackerEntity(BaseTrackerEntity):
|
||||
"""Return the device state attributes."""
|
||||
attr: dict[str, StateType] = {}
|
||||
attr.update(super().state_attributes)
|
||||
|
||||
if self.latitude is not None and self.longitude is not None:
|
||||
attr[ATTR_LATITUDE] = self.latitude
|
||||
attr[ATTR_LONGITUDE] = self.longitude
|
||||
|
||||
32
homeassistant/components/esphome/diagnostics.py
Normal file
32
homeassistant/components/esphome/diagnostics.py
Normal file
@@ -0,0 +1,32 @@
|
||||
"""Diahgnostics support for ESPHome."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, cast
|
||||
|
||||
from homeassistant.components.diagnostics import async_redact_data
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_PASSWORD
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from . import CONF_NOISE_PSK, DomainData
|
||||
|
||||
CONF_MAC_ADDRESS = "mac_address"
|
||||
|
||||
REDACT_KEYS = {CONF_NOISE_PSK, CONF_PASSWORD, CONF_MAC_ADDRESS}
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant, config_entry: ConfigEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
diag: dict[str, Any] = {}
|
||||
|
||||
diag["config"] = config_entry.as_dict()
|
||||
|
||||
entry_data = DomainData.get(hass).get_entry_data(config_entry)
|
||||
|
||||
if (storage_data := await entry_data.store.async_load()) is not None:
|
||||
storage_data = cast("dict[str, Any]", storage_data)
|
||||
diag["storage_data"] = storage_data
|
||||
|
||||
return async_redact_data(diag, REDACT_KEYS)
|
||||
@@ -4,7 +4,7 @@
|
||||
"config_flow": true,
|
||||
"dependencies": ["network"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/flux_led",
|
||||
"requirements": ["flux_led==0.28.11"],
|
||||
"requirements": ["flux_led==0.28.17"],
|
||||
"quality_scale": "platinum",
|
||||
"codeowners": ["@icemanch", "@bdraco"],
|
||||
"iot_class": "local_push",
|
||||
|
||||
@@ -14,6 +14,7 @@ from fritzconnection.core.exceptions import (
|
||||
FritzActionError,
|
||||
FritzActionFailedError,
|
||||
FritzConnectionException,
|
||||
FritzInternalError,
|
||||
FritzLookUpError,
|
||||
FritzSecurityError,
|
||||
FritzServiceError,
|
||||
@@ -523,6 +524,7 @@ class AvmWrapper(FritzBoxTools):
|
||||
except (
|
||||
FritzActionError,
|
||||
FritzActionFailedError,
|
||||
FritzInternalError,
|
||||
FritzServiceError,
|
||||
FritzLookUpError,
|
||||
):
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Home Assistant Frontend",
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"requirements": [
|
||||
"home-assistant-frontend==20220126.0"
|
||||
"home-assistant-frontend==20220127.0"
|
||||
],
|
||||
"dependencies": [
|
||||
"api",
|
||||
|
||||
@@ -76,7 +76,7 @@ async def async_setup_entry(
|
||||
for description in NUMBERS:
|
||||
try:
|
||||
current_value = await description.getter(inverter)
|
||||
except InverterError:
|
||||
except (InverterError, ValueError):
|
||||
# Inverter model does not support this setting
|
||||
_LOGGER.debug("Could not read inverter setting %s", description.key)
|
||||
continue
|
||||
|
||||
@@ -42,7 +42,7 @@ async def async_setup_entry(
|
||||
# read current operating mode from the inverter
|
||||
try:
|
||||
active_mode = await inverter.get_operation_mode()
|
||||
except InverterError:
|
||||
except (InverterError, ValueError):
|
||||
# Inverter model does not support this setting
|
||||
_LOGGER.debug("Could not read inverter operation mode")
|
||||
else:
|
||||
|
||||
@@ -294,6 +294,15 @@ async def async_devices_reachable(hass, data: RequestData, payload):
|
||||
}
|
||||
|
||||
|
||||
@HANDLERS.register("action.devices.PROXY_SELECTED")
|
||||
async def async_devices_proxy_selected(hass, data: RequestData, payload):
|
||||
"""Handle action.devices.PROXY_SELECTED request.
|
||||
|
||||
When selected for local SDK.
|
||||
"""
|
||||
return {}
|
||||
|
||||
|
||||
def turned_off_response(message):
|
||||
"""Return a device turned off response."""
|
||||
return {
|
||||
|
||||
@@ -154,7 +154,11 @@ class HassIOIngress(HomeAssistantView):
|
||||
async for data in result.content.iter_chunked(4096):
|
||||
await response.write(data)
|
||||
|
||||
except (aiohttp.ClientError, aiohttp.ClientPayloadError) as err:
|
||||
except (
|
||||
aiohttp.ClientError,
|
||||
aiohttp.ClientPayloadError,
|
||||
ConnectionResetError,
|
||||
) as err:
|
||||
_LOGGER.debug("Stream error %s / %s: %s", token, path, err)
|
||||
|
||||
return response
|
||||
|
||||
@@ -190,7 +190,7 @@ class HumidifierDehumidifier(HomeAccessory):
|
||||
)
|
||||
self.char_current_humidity.set_value(current_humidity)
|
||||
except ValueError as ex:
|
||||
_LOGGER.error(
|
||||
_LOGGER.debug(
|
||||
"%s: Unable to update from linked humidity sensor %s: %s",
|
||||
self.entity_id,
|
||||
self.linked_humidity_sensor,
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import aiohomekit
|
||||
@@ -26,6 +27,8 @@ from .connection import HKDevice, valid_serial_number
|
||||
from .const import CONTROLLER, ENTITY_MAP, KNOWN_DEVICES, TRIGGERS
|
||||
from .storage import EntityMapStorage
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def escape_characteristic_name(char_name):
|
||||
"""Escape any dash or dots in a characteristics name."""
|
||||
@@ -248,4 +251,21 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Cleanup caches before removing config entry."""
|
||||
hkid = entry.data["AccessoryPairingID"]
|
||||
|
||||
# Remove cached type data from .storage/homekit_controller-entity-map
|
||||
hass.data[ENTITY_MAP].async_delete_map(hkid)
|
||||
|
||||
# Remove the pairing on the device, making the device discoverable again.
|
||||
# Don't reuse any objects in hass.data as they are already unloaded
|
||||
async_zeroconf_instance = await zeroconf.async_get_async_instance(hass)
|
||||
controller = aiohomekit.Controller(async_zeroconf_instance=async_zeroconf_instance)
|
||||
controller.load_pairing(hkid, dict(entry.data))
|
||||
try:
|
||||
await controller.remove_pairing(hkid)
|
||||
except aiohomekit.AccessoryDisconnectedError:
|
||||
_LOGGER.warning(
|
||||
"Accessory %s was removed from HomeAssistant but was not reachable "
|
||||
"to properly unpair. It may need resetting before you can use it with "
|
||||
"HomeKit again",
|
||||
entry.title,
|
||||
)
|
||||
|
||||
@@ -44,21 +44,21 @@ class HomeKitSensorEntityDescription(SensorEntityDescription):
|
||||
SIMPLE_SENSOR: dict[str, HomeKitSensorEntityDescription] = {
|
||||
CharacteristicsTypes.Vendor.CONNECTSENSE_ENERGY_WATT: HomeKitSensorEntityDescription(
|
||||
key=CharacteristicsTypes.Vendor.CONNECTSENSE_ENERGY_WATT,
|
||||
name="Real Time Energy",
|
||||
name="Power",
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=POWER_WATT,
|
||||
),
|
||||
CharacteristicsTypes.Vendor.CONNECTSENSE_ENERGY_AMPS: HomeKitSensorEntityDescription(
|
||||
key=CharacteristicsTypes.Vendor.CONNECTSENSE_ENERGY_AMPS,
|
||||
name="Real Time Current",
|
||||
name="Current",
|
||||
device_class=SensorDeviceClass.CURRENT,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE,
|
||||
),
|
||||
CharacteristicsTypes.Vendor.CONNECTSENSE_ENERGY_AMPS_20: HomeKitSensorEntityDescription(
|
||||
key=CharacteristicsTypes.Vendor.CONNECTSENSE_ENERGY_AMPS_20,
|
||||
name="Real Time Current",
|
||||
name="Current",
|
||||
device_class=SensorDeviceClass.CURRENT,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE,
|
||||
@@ -72,7 +72,7 @@ SIMPLE_SENSOR: dict[str, HomeKitSensorEntityDescription] = {
|
||||
),
|
||||
CharacteristicsTypes.Vendor.EVE_ENERGY_WATT: HomeKitSensorEntityDescription(
|
||||
key=CharacteristicsTypes.Vendor.EVE_ENERGY_WATT,
|
||||
name="Real Time Energy",
|
||||
name="Power",
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=POWER_WATT,
|
||||
@@ -100,14 +100,14 @@ SIMPLE_SENSOR: dict[str, HomeKitSensorEntityDescription] = {
|
||||
),
|
||||
CharacteristicsTypes.Vendor.KOOGEEK_REALTIME_ENERGY: HomeKitSensorEntityDescription(
|
||||
key=CharacteristicsTypes.Vendor.KOOGEEK_REALTIME_ENERGY,
|
||||
name="Real Time Energy",
|
||||
name="Power",
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=POWER_WATT,
|
||||
),
|
||||
CharacteristicsTypes.Vendor.KOOGEEK_REALTIME_ENERGY_2: HomeKitSensorEntityDescription(
|
||||
key=CharacteristicsTypes.Vendor.KOOGEEK_REALTIME_ENERGY_2,
|
||||
name="Real Time Energy",
|
||||
name="Power",
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=POWER_WATT,
|
||||
@@ -121,7 +121,7 @@ SIMPLE_SENSOR: dict[str, HomeKitSensorEntityDescription] = {
|
||||
),
|
||||
CharacteristicsTypes.Vendor.VOCOLINC_OUTLET_ENERGY: HomeKitSensorEntityDescription(
|
||||
key=CharacteristicsTypes.Vendor.VOCOLINC_OUTLET_ENERGY,
|
||||
name="Real Time Energy",
|
||||
name="Power",
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=POWER_WATT,
|
||||
|
||||
@@ -127,6 +127,10 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
|
||||
self._set_confirm_only()
|
||||
|
||||
self.context["title_placeholders"] = {
|
||||
"name": f"{self.config[CONF_PRODUCT_NAME]} ({self.config[CONF_SERIAL]})"
|
||||
}
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="discovery_confirm",
|
||||
description_placeholders={
|
||||
|
||||
@@ -15,10 +15,10 @@
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"invalid_discovery_parameters": "unsupported_api_version",
|
||||
"invalid_discovery_parameters": "Detected unsupported API version",
|
||||
"api_not_enabled": "The API is not enabled. Enable API in the HomeWizard Energy App under settings",
|
||||
"device_not_supported": "This device is not supported",
|
||||
"unknown_error": "[%key:common::config_flow::error::unknown%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ SENSOR_DESCRIPTIONS: tuple[LaunchLibrarySensorEntityDescription, ...] = (
|
||||
LaunchLibrarySensorEntityDescription(
|
||||
key="launch_probability",
|
||||
icon="mdi:dice-multiple",
|
||||
name="Launch Probability",
|
||||
name="Launch probability",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
value_fn=lambda nl: None if nl.probability == -1 else nl.probability,
|
||||
attributes_fn=lambda nl: None,
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Mazda Connected Services",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/mazda",
|
||||
"requirements": ["pymazda==0.3.1"],
|
||||
"requirements": ["pymazda==0.3.2"],
|
||||
"codeowners": ["@bdr99"],
|
||||
"quality_scale": "platinum",
|
||||
"iot_class": "cloud_polling"
|
||||
|
||||
@@ -125,6 +125,8 @@ CONF_TEMP_MAX = "max_temp"
|
||||
CONF_TEMP_MIN = "min_temp"
|
||||
CONF_TEMP_STEP = "temp_step"
|
||||
|
||||
PAYLOAD_NONE = "None"
|
||||
|
||||
MQTT_CLIMATE_ATTRIBUTES_BLOCKED = frozenset(
|
||||
{
|
||||
climate.ATTR_AUX_HEAT,
|
||||
@@ -441,6 +443,12 @@ class MqttClimate(MqttEntity, ClimateEntity):
|
||||
if payload in CURRENT_HVAC_ACTIONS:
|
||||
self._action = payload
|
||||
self.async_write_ha_state()
|
||||
elif not payload or payload == PAYLOAD_NONE:
|
||||
_LOGGER.debug(
|
||||
"Invalid %s action: %s, ignoring",
|
||||
CURRENT_HVAC_ACTIONS,
|
||||
payload,
|
||||
)
|
||||
else:
|
||||
_LOGGER.warning(
|
||||
"Invalid %s action: %s",
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"config_flow": true,
|
||||
"dependencies": ["ffmpeg", "http", "media_source"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/nest",
|
||||
"requirements": ["python-nest==4.1.0", "google-nest-sdm==1.5.1"],
|
||||
"requirements": ["python-nest==4.1.0", "google-nest-sdm==1.6.0"],
|
||||
"codeowners": ["@allenporter"],
|
||||
"quality_scale": "platinum",
|
||||
"dhcp": [
|
||||
|
||||
33
homeassistant/components/onewire/diagnostics.py
Normal file
33
homeassistant/components/onewire/diagnostics.py
Normal file
@@ -0,0 +1,33 @@
|
||||
"""Diagnostics support for 1-Wire."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import asdict
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.diagnostics import async_redact_data
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_HOST
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import DOMAIN
|
||||
from .onewirehub import OneWireHub
|
||||
|
||||
TO_REDACT = {CONF_HOST}
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant, entry: ConfigEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
onewirehub: OneWireHub = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
return {
|
||||
"entry": {
|
||||
"title": entry.title,
|
||||
"data": async_redact_data(entry.data, TO_REDACT),
|
||||
"options": {**entry.options},
|
||||
},
|
||||
"devices": [asdict(device_details) for device_details in onewirehub.devices]
|
||||
if onewirehub.devices
|
||||
else [],
|
||||
}
|
||||
@@ -48,7 +48,8 @@ class Awning(OverkizGenericCover):
|
||||
|
||||
None is unknown, 0 is closed, 100 is fully open.
|
||||
"""
|
||||
if current_position := self.executor.select_state(OverkizState.CORE_DEPLOYMENT):
|
||||
current_position = self.executor.select_state(OverkizState.CORE_DEPLOYMENT)
|
||||
if current_position is not None:
|
||||
return cast(int, current_position)
|
||||
|
||||
return None
|
||||
|
||||
@@ -51,9 +51,10 @@ class OverkizGenericCover(OverkizEntity, CoverEntity):
|
||||
|
||||
None is unknown, 0 is closed, 100 is fully open.
|
||||
"""
|
||||
if position := self.executor.select_state(
|
||||
position = self.executor.select_state(
|
||||
OverkizState.CORE_SLATS_ORIENTATION, OverkizState.CORE_SLATE_ORIENTATION
|
||||
):
|
||||
)
|
||||
if position is not None:
|
||||
return 100 - cast(int, position)
|
||||
|
||||
return None
|
||||
|
||||
@@ -79,8 +79,9 @@ class OverkizLight(OverkizEntity, LightEntity):
|
||||
@property
|
||||
def brightness(self) -> int | None:
|
||||
"""Return the brightness of this light (0-255)."""
|
||||
if brightness := self.executor.select_state(OverkizState.CORE_LIGHT_INTENSITY):
|
||||
return round(cast(int, brightness) * 255 / 100)
|
||||
value = self.executor.select_state(OverkizState.CORE_LIGHT_INTENSITY)
|
||||
if value is not None:
|
||||
return round(cast(int, value) * 255 / 100)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
35
homeassistant/components/p1_monitor/diagnostics.py
Normal file
35
homeassistant/components/p1_monitor/diagnostics.py
Normal file
@@ -0,0 +1,35 @@
|
||||
"""Diagnostics support for P1 Monitor."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.diagnostics import async_redact_data
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_HOST
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from . import P1MonitorDataUpdateCoordinator
|
||||
from .const import DOMAIN, SERVICE_PHASES, SERVICE_SETTINGS, SERVICE_SMARTMETER
|
||||
|
||||
TO_REDACT = {
|
||||
CONF_HOST,
|
||||
}
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant, entry: ConfigEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
coordinator: P1MonitorDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
return {
|
||||
"entry": {
|
||||
"title": entry.title,
|
||||
"data": async_redact_data(entry.data, TO_REDACT),
|
||||
},
|
||||
"data": {
|
||||
"smartmeter": coordinator.data[SERVICE_SMARTMETER].__dict__,
|
||||
"phases": coordinator.data[SERVICE_PHASES].__dict__,
|
||||
"settings": coordinator.data[SERVICE_SETTINGS].__dict__,
|
||||
},
|
||||
}
|
||||
@@ -140,7 +140,7 @@ class PingBinarySensor(RestoreEntity, BinarySensorEntity):
|
||||
self._available = True
|
||||
|
||||
if last_state is None or last_state.state != STATE_ON:
|
||||
self._ping.data = False
|
||||
self._ping.data = None
|
||||
return
|
||||
|
||||
attributes = last_state.attributes
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/renault",
|
||||
"requirements": [
|
||||
"renault-api==0.1.4"
|
||||
"renault-api==0.1.7"
|
||||
],
|
||||
"codeowners": [
|
||||
"@epenet"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "roku",
|
||||
"name": "Roku",
|
||||
"documentation": "https://www.home-assistant.io/integrations/roku",
|
||||
"requirements": ["rokuecp==0.11.0"],
|
||||
"requirements": ["rokuecp==0.12.0"],
|
||||
"homekit": {
|
||||
"models": ["3810X", "4660X", "7820X", "C105X", "C135X"]
|
||||
},
|
||||
|
||||
@@ -408,13 +408,13 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity):
|
||||
if attr in extra
|
||||
}
|
||||
|
||||
await self.coordinator.roku.play_video(media_id, params)
|
||||
await self.coordinator.roku.play_on_roku(media_id, params)
|
||||
elif media_type == FORMAT_CONTENT_TYPE[HLS_PROVIDER]:
|
||||
params = {
|
||||
"MediaType": "hls",
|
||||
}
|
||||
|
||||
await self.coordinator.roku.play_video(media_id, params)
|
||||
await self.coordinator.roku.play_on_roku(media_id, params)
|
||||
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
|
||||
17
homeassistant/components/rtsp_to_webrtc/diagnostics.py
Normal file
17
homeassistant/components/rtsp_to_webrtc/diagnostics.py
Normal file
@@ -0,0 +1,17 @@
|
||||
"""Diagnostics support for Nest."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from rtsp_to_webrtc import client
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant, config_entry: ConfigEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
return dict(client.get_diagnostics())
|
||||
@@ -562,6 +562,11 @@ class ShellySleepingBlockAttributeEntity(ShellyBlockAttributeEntity, RestoreEnti
|
||||
self.block: Block | None = block # type: ignore[assignment]
|
||||
self.entity_description = description
|
||||
|
||||
self._attr_should_poll = False
|
||||
self._attr_device_info = DeviceInfo(
|
||||
connections={(device_registry.CONNECTION_NETWORK_MAC, wrapper.mac)}
|
||||
)
|
||||
|
||||
if block is not None:
|
||||
self._attr_unique_id = f"{self.wrapper.mac}-{block.description}-{attribute}"
|
||||
self._attr_name = get_block_entity_name(
|
||||
|
||||
@@ -223,7 +223,7 @@ SENSORS: Final = {
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
available=lambda block: cast(int, block.extTemp) != 999
|
||||
and not block.sensorError,
|
||||
and not getattr(block, "sensorError", False),
|
||||
),
|
||||
("sensor", "humidity"): BlockSensorDescription(
|
||||
key="sensor|humidity",
|
||||
@@ -233,7 +233,7 @@ SENSORS: Final = {
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
available=lambda block: cast(int, block.humidity) != 999
|
||||
and not block.sensorError,
|
||||
and not getattr(block, "sensorError", False),
|
||||
),
|
||||
("sensor", "luminosity"): BlockSensorDescription(
|
||||
key="sensor|luminosity",
|
||||
|
||||
@@ -125,15 +125,22 @@ def get_block_channel_name(device: BlockDevice, block: Block | None) -> str:
|
||||
return f"{entity_name} channel {chr(int(block.channel)+base)}"
|
||||
|
||||
|
||||
def is_block_momentary_input(settings: dict[str, Any], block: Block) -> bool:
|
||||
def is_block_momentary_input(
|
||||
settings: dict[str, Any], block: Block, include_detached: bool = False
|
||||
) -> bool:
|
||||
"""Return true if block input button settings is set to a momentary type."""
|
||||
momentary_types = ["momentary", "momentary_on_release"]
|
||||
|
||||
if include_detached:
|
||||
momentary_types.append("detached")
|
||||
|
||||
# Shelly Button type is fixed to momentary and no btn_type
|
||||
if settings["device"]["type"] in SHBTN_MODELS:
|
||||
return True
|
||||
|
||||
if settings.get("mode") == "roller":
|
||||
button_type = settings["rollers"][0]["button_type"]
|
||||
return button_type in ["momentary", "momentary_on_release"]
|
||||
return button_type in momentary_types
|
||||
|
||||
button = settings.get("relays") or settings.get("lights") or settings.get("inputs")
|
||||
if button is None:
|
||||
@@ -148,7 +155,7 @@ def is_block_momentary_input(settings: dict[str, Any], block: Block) -> bool:
|
||||
channel = min(int(block.channel or 0), len(button) - 1)
|
||||
button_type = button[channel].get("btn_type")
|
||||
|
||||
return button_type in ["momentary", "momentary_on_release"]
|
||||
return button_type in momentary_types
|
||||
|
||||
|
||||
def get_device_uptime(uptime: float, last_uptime: datetime | None) -> datetime:
|
||||
@@ -171,7 +178,7 @@ def get_block_input_triggers(
|
||||
if "inputEvent" not in block.sensor_ids or "inputEventCnt" not in block.sensor_ids:
|
||||
return []
|
||||
|
||||
if not is_block_momentary_input(device.settings, block):
|
||||
if not is_block_momentary_input(device.settings, block, True):
|
||||
return []
|
||||
|
||||
triggers = []
|
||||
|
||||
@@ -4,6 +4,7 @@ import aiohttp
|
||||
from spotipy import Spotify, SpotifyException
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.media_player import BrowseError
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
ATTR_CREDENTIALS,
|
||||
@@ -60,7 +61,8 @@ async def async_browse_media(
|
||||
hass, media_content_type, media_content_id, *, can_play_artist=True
|
||||
):
|
||||
"""Browse Spotify media."""
|
||||
info = list(hass.data[DOMAIN].values())[0]
|
||||
if not (info := next(iter(hass.data[DOMAIN].values()), None)):
|
||||
raise BrowseError("No Spotify accounts available")
|
||||
return await async_browse_media_internal(
|
||||
hass,
|
||||
info[DATA_SPOTIFY_CLIENT],
|
||||
|
||||
@@ -74,7 +74,16 @@ class IntegerTypeData:
|
||||
@classmethod
|
||||
def from_json(cls, dpcode: DPCode, data: str) -> IntegerTypeData:
|
||||
"""Load JSON string and return a IntegerTypeData object."""
|
||||
return cls(dpcode, **json.loads(data))
|
||||
parsed = json.loads(data)
|
||||
return cls(
|
||||
dpcode,
|
||||
min=int(parsed["min"]),
|
||||
max=int(parsed["max"]),
|
||||
scale=float(parsed["scale"]),
|
||||
step=float(parsed["step"]),
|
||||
unit=parsed.get("unit"),
|
||||
type=parsed.get("type"),
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
|
||||
@@ -57,6 +57,9 @@ async def async_reconnect_client(hass, data) -> None:
|
||||
device_registry = dr.async_get(hass)
|
||||
device_entry = device_registry.async_get(data[ATTR_DEVICE_ID])
|
||||
|
||||
if device_entry is None:
|
||||
return
|
||||
|
||||
mac = ""
|
||||
for connection in device_entry.connections:
|
||||
if connection[0] == CONNECTION_NETWORK_MAC:
|
||||
|
||||
@@ -101,6 +101,15 @@ def _build_entity(name, vicare_api, circuit, device_config, heating_type):
|
||||
return ViCareClimate(name, vicare_api, device_config, circuit, heating_type)
|
||||
|
||||
|
||||
def _get_circuits(vicare_api):
|
||||
"""Return the list of circuits."""
|
||||
try:
|
||||
return vicare_api.circuits
|
||||
except PyViCareNotSupportedFeatureError:
|
||||
_LOGGER.info("No circuits found")
|
||||
return []
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
@@ -108,25 +117,23 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up the ViCare climate platform."""
|
||||
name = VICARE_NAME
|
||||
|
||||
entities = []
|
||||
api = hass.data[DOMAIN][config_entry.entry_id][VICARE_API]
|
||||
circuits = await hass.async_add_executor_job(_get_circuits, api)
|
||||
|
||||
try:
|
||||
for circuit in hass.data[DOMAIN][config_entry.entry_id][VICARE_API].circuits:
|
||||
suffix = ""
|
||||
if len(hass.data[DOMAIN][config_entry.entry_id][VICARE_API].circuits) > 1:
|
||||
suffix = f" {circuit.id}"
|
||||
entity = _build_entity(
|
||||
f"{name} Heating{suffix}",
|
||||
hass.data[DOMAIN][config_entry.entry_id][VICARE_API],
|
||||
hass.data[DOMAIN][config_entry.entry_id][VICARE_DEVICE_CONFIG],
|
||||
circuit,
|
||||
config_entry.data[CONF_HEATING_TYPE],
|
||||
)
|
||||
if entity is not None:
|
||||
entities.append(entity)
|
||||
except PyViCareNotSupportedFeatureError:
|
||||
_LOGGER.info("No circuits found")
|
||||
for circuit in circuits:
|
||||
suffix = ""
|
||||
if len(circuits) > 1:
|
||||
suffix = f" {circuit.id}"
|
||||
|
||||
entity = _build_entity(
|
||||
f"{name} Heating{suffix}",
|
||||
api,
|
||||
hass.data[DOMAIN][config_entry.entry_id][VICARE_DEVICE_CONFIG],
|
||||
circuit,
|
||||
config_entry.data[CONF_HEATING_TYPE],
|
||||
)
|
||||
entities.append(entity)
|
||||
|
||||
platform = entity_platform.async_get_current_platform()
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Viessmann ViCare",
|
||||
"documentation": "https://www.home-assistant.io/integrations/vicare",
|
||||
"codeowners": ["@oischinger"],
|
||||
"requirements": ["PyViCare==2.15.0"],
|
||||
"requirements": ["PyViCare==2.16.1"],
|
||||
"iot_class": "cloud_polling",
|
||||
"config_flow": true,
|
||||
"dhcp": [
|
||||
|
||||
@@ -68,6 +68,15 @@ def _build_entity(name, vicare_api, circuit, device_config, heating_type):
|
||||
)
|
||||
|
||||
|
||||
def _get_circuits(vicare_api):
|
||||
"""Return the list of circuits."""
|
||||
try:
|
||||
return vicare_api.circuits
|
||||
except PyViCareNotSupportedFeatureError:
|
||||
_LOGGER.info("No circuits found")
|
||||
return []
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
@@ -75,24 +84,23 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up the ViCare climate platform."""
|
||||
name = VICARE_NAME
|
||||
|
||||
entities = []
|
||||
try:
|
||||
for circuit in hass.data[DOMAIN][config_entry.entry_id][VICARE_API].circuits:
|
||||
suffix = ""
|
||||
if len(hass.data[DOMAIN][config_entry.entry_id][VICARE_API].circuits) > 1:
|
||||
suffix = f" {circuit.id}"
|
||||
entity = _build_entity(
|
||||
f"{name} Water{suffix}",
|
||||
hass.data[DOMAIN][config_entry.entry_id][VICARE_API],
|
||||
circuit,
|
||||
hass.data[DOMAIN][config_entry.entry_id][VICARE_DEVICE_CONFIG],
|
||||
config_entry.data[CONF_HEATING_TYPE],
|
||||
)
|
||||
if entity is not None:
|
||||
entities.append(entity)
|
||||
except PyViCareNotSupportedFeatureError:
|
||||
_LOGGER.info("No circuits found")
|
||||
api = hass.data[DOMAIN][config_entry.entry_id][VICARE_API]
|
||||
circuits = await hass.async_add_executor_job(_get_circuits, api)
|
||||
|
||||
for circuit in circuits:
|
||||
suffix = ""
|
||||
if len(circuits) > 1:
|
||||
suffix = f" {circuit.id}"
|
||||
|
||||
entity = _build_entity(
|
||||
f"{name} Water{suffix}",
|
||||
api,
|
||||
circuit,
|
||||
hass.data[DOMAIN][config_entry.entry_id][VICARE_DEVICE_CONFIG],
|
||||
config_entry.data[CONF_HEATING_TYPE],
|
||||
)
|
||||
entities.append(entity)
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import asyncio
|
||||
|
||||
from aiohttp.web import Request, Response
|
||||
import voluptuous as vol
|
||||
from withings_api import WithingsAuth
|
||||
from withings_api import AbstractWithingsApi, WithingsAuth
|
||||
from withings_api.common import NotifyAppli
|
||||
|
||||
from homeassistant.components import webhook
|
||||
@@ -84,7 +84,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
conf[CONF_CLIENT_ID],
|
||||
conf[CONF_CLIENT_SECRET],
|
||||
f"{WithingsAuth.URL}/oauth2_user/authorize2",
|
||||
f"{WithingsAuth.URL}/oauth2/token",
|
||||
f"{AbstractWithingsApi.URL}/v2/oauth2",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -1111,3 +1111,46 @@ class WithingsLocalOAuth2Implementation(LocalOAuth2Implementation):
|
||||
"""Return the redirect uri."""
|
||||
url = get_url(self.hass, allow_internal=False, prefer_cloud=True)
|
||||
return f"{url}{AUTH_CALLBACK_PATH}"
|
||||
|
||||
async def _token_request(self, data: dict) -> dict:
|
||||
"""Make a token request and adapt Withings API reply."""
|
||||
new_token = await super()._token_request(data)
|
||||
# Withings API returns habitual token data under json key "body":
|
||||
# {
|
||||
# "status": [{integer} Withings API response status],
|
||||
# "body": {
|
||||
# "access_token": [{string} Your new access_token],
|
||||
# "expires_in": [{integer} Access token expiry delay in seconds],
|
||||
# "token_type": [{string] HTTP Authorization Header format: Bearer],
|
||||
# "scope": [{string} Scopes the user accepted],
|
||||
# "refresh_token": [{string} Your new refresh_token],
|
||||
# "userid": [{string} The Withings ID of the user]
|
||||
# }
|
||||
# }
|
||||
# so we copy that to token root.
|
||||
if body := new_token.pop("body", None):
|
||||
new_token.update(body)
|
||||
return new_token
|
||||
|
||||
async def async_resolve_external_data(self, external_data: Any) -> dict:
|
||||
"""Resolve the authorization code to tokens."""
|
||||
return await self._token_request(
|
||||
{
|
||||
"action": "requesttoken",
|
||||
"grant_type": "authorization_code",
|
||||
"code": external_data["code"],
|
||||
"redirect_uri": external_data["state"]["redirect_uri"],
|
||||
}
|
||||
)
|
||||
|
||||
async def _async_refresh_token(self, token: dict) -> dict:
|
||||
"""Refresh tokens."""
|
||||
new_token = await self._token_request(
|
||||
{
|
||||
"action": "requesttoken",
|
||||
"grant_type": "refresh_token",
|
||||
"client_id": self.client_id,
|
||||
"refresh_token": token["refresh_token"],
|
||||
}
|
||||
)
|
||||
return {**token, **new_token}
|
||||
|
||||
@@ -15,7 +15,6 @@ async def async_setup_entry(
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the sensor config entry."""
|
||||
|
||||
entities = await async_create_entities(
|
||||
hass,
|
||||
entry,
|
||||
|
||||
@@ -161,7 +161,10 @@ class YaleOptionsFlowHandler(OptionsFlow):
|
||||
errors = {}
|
||||
|
||||
if user_input:
|
||||
if len(user_input[CONF_CODE]) not in [0, user_input[CONF_LOCK_CODE_DIGITS]]:
|
||||
if len(user_input.get(CONF_CODE, "")) not in [
|
||||
0,
|
||||
user_input[CONF_LOCK_CODE_DIGITS],
|
||||
]:
|
||||
errors["base"] = "code_format_mismatch"
|
||||
else:
|
||||
return self.async_create_entry(title="", data=user_input)
|
||||
@@ -171,7 +174,10 @@ class YaleOptionsFlowHandler(OptionsFlow):
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Optional(
|
||||
CONF_CODE, default=self.entry.options.get(CONF_CODE)
|
||||
CONF_CODE,
|
||||
description={
|
||||
"suggested_value": self.entry.options.get(CONF_CODE)
|
||||
},
|
||||
): str,
|
||||
vol.Optional(
|
||||
CONF_LOCK_CODE_DIGITS,
|
||||
|
||||
@@ -10,6 +10,7 @@ import voluptuous as vol
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.const import (
|
||||
ATTR_EDITABLE,
|
||||
ATTR_GPS_ACCURACY,
|
||||
ATTR_LATITUDE,
|
||||
ATTR_LONGITUDE,
|
||||
CONF_ICON,
|
||||
@@ -22,14 +23,7 @@ from homeassistant.const import (
|
||||
SERVICE_RELOAD,
|
||||
STATE_UNAVAILABLE,
|
||||
)
|
||||
from homeassistant.core import (
|
||||
Event,
|
||||
HomeAssistant,
|
||||
ServiceCall,
|
||||
State,
|
||||
callback,
|
||||
split_entity_id,
|
||||
)
|
||||
from homeassistant.core import Event, HomeAssistant, ServiceCall, State, callback
|
||||
from homeassistant.helpers import (
|
||||
collection,
|
||||
config_validation as cv,
|
||||
@@ -346,10 +340,20 @@ class Zone(entity.Entity):
|
||||
|
||||
@callback
|
||||
def _person_state_change_listener(self, evt: Event) -> None:
|
||||
object_id = split_entity_id(self.entity_id)[1]
|
||||
person_entity_id = evt.data["entity_id"]
|
||||
cur_count = len(self._persons_in_zone)
|
||||
if evt.data["new_state"] and evt.data["new_state"].state == object_id:
|
||||
if (
|
||||
(state := evt.data["new_state"])
|
||||
and (latitude := state.attributes.get(ATTR_LATITUDE)) is not None
|
||||
and (longitude := state.attributes.get(ATTR_LONGITUDE)) is not None
|
||||
and (accuracy := state.attributes.get(ATTR_GPS_ACCURACY)) is not None
|
||||
and (
|
||||
zone_state := async_active_zone(
|
||||
self.hass, latitude, longitude, accuracy
|
||||
)
|
||||
)
|
||||
and zone_state.entity_id == self.entity_id
|
||||
):
|
||||
self._persons_in_zone.add(person_entity_id)
|
||||
elif person_entity_id in self._persons_in_zone:
|
||||
self._persons_in_zone.remove(person_entity_id)
|
||||
@@ -362,10 +366,17 @@ class Zone(entity.Entity):
|
||||
await super().async_added_to_hass()
|
||||
person_domain = "person" # avoid circular import
|
||||
persons = self.hass.states.async_entity_ids(person_domain)
|
||||
object_id = split_entity_id(self.entity_id)[1]
|
||||
for person in persons:
|
||||
state = self.hass.states.get(person)
|
||||
if state and state.state == object_id:
|
||||
if (
|
||||
state is None
|
||||
or (latitude := state.attributes.get(ATTR_LATITUDE)) is None
|
||||
or (longitude := state.attributes.get(ATTR_LONGITUDE)) is None
|
||||
or (accuracy := state.attributes.get(ATTR_GPS_ACCURACY)) is None
|
||||
):
|
||||
continue
|
||||
zone_state = async_active_zone(self.hass, latitude, longitude, accuracy)
|
||||
if zone_state is not None and zone_state.entity_id == self.entity_id:
|
||||
self._persons_in_zone.add(person)
|
||||
|
||||
self.async_on_remove(
|
||||
|
||||
@@ -7,7 +7,7 @@ from .backports.enum import StrEnum
|
||||
|
||||
MAJOR_VERSION: Final = 2022
|
||||
MINOR_VERSION: Final = 2
|
||||
PATCH_VERSION: Final = "0b0"
|
||||
PATCH_VERSION: Final = "0b2"
|
||||
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
||||
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0)
|
||||
|
||||
@@ -9,14 +9,13 @@ async_timeout==4.0.2
|
||||
atomicwrites==1.4.0
|
||||
attrs==21.2.0
|
||||
awesomeversion==22.1.0
|
||||
backports.zoneinfo;python_version<"3.9"
|
||||
bcrypt==3.1.7
|
||||
certifi>=2021.5.30
|
||||
ciso8601==2.2.0
|
||||
cryptography==35.0.0
|
||||
emoji==1.6.3
|
||||
hass-nabucasa==0.52.0
|
||||
home-assistant-frontend==20220126.0
|
||||
home-assistant-frontend==20220127.0
|
||||
httpx==0.21.3
|
||||
ifaddr==0.1.7
|
||||
jinja2==3.0.3
|
||||
|
||||
@@ -5,16 +5,11 @@ import bisect
|
||||
from contextlib import suppress
|
||||
import datetime as dt
|
||||
import re
|
||||
import sys
|
||||
from typing import Any, cast
|
||||
from typing import Any
|
||||
import zoneinfo
|
||||
|
||||
import ciso8601
|
||||
|
||||
if sys.version_info[:2] >= (3, 9):
|
||||
import zoneinfo
|
||||
else:
|
||||
from backports import zoneinfo
|
||||
|
||||
DATE_STR_FORMAT = "%Y-%m-%d"
|
||||
UTC = dt.timezone.utc
|
||||
DEFAULT_TIME_ZONE: dt.tzinfo = dt.timezone.utc
|
||||
@@ -48,8 +43,7 @@ def get_time_zone(time_zone_str: str) -> dt.tzinfo | None:
|
||||
Async friendly.
|
||||
"""
|
||||
try:
|
||||
# Cast can be removed when mypy is switched to Python 3.9.
|
||||
return cast(dt.tzinfo, zoneinfo.ZoneInfo(time_zone_str))
|
||||
return zoneinfo.ZoneInfo(time_zone_str)
|
||||
except zoneinfo.ZoneInfoNotFoundError:
|
||||
return None
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ async_timeout==4.0.2
|
||||
attrs==21.2.0
|
||||
atomicwrites==1.4.0
|
||||
awesomeversion==22.1.0
|
||||
backports.zoneinfo;python_version<"3.9"
|
||||
bcrypt==3.1.7
|
||||
certifi>=2021.5.30
|
||||
ciso8601==2.2.0
|
||||
|
||||
@@ -56,7 +56,7 @@ PyTransportNSW==0.1.1
|
||||
PyTurboJPEG==1.6.5
|
||||
|
||||
# homeassistant.components.vicare
|
||||
PyViCare==2.15.0
|
||||
PyViCare==2.16.1
|
||||
|
||||
# homeassistant.components.xiaomi_aqara
|
||||
PyXiaomiGateway==0.13.4
|
||||
@@ -338,7 +338,7 @@ aqualogic==2.6
|
||||
arcam-fmj==0.12.0
|
||||
|
||||
# homeassistant.components.arris_tg2492lg
|
||||
arris-tg2492lg==1.1.0
|
||||
arris-tg2492lg==1.2.1
|
||||
|
||||
# homeassistant.components.ampio
|
||||
asmog==0.0.6
|
||||
@@ -681,7 +681,7 @@ fjaraskupan==1.0.2
|
||||
flipr-api==1.4.1
|
||||
|
||||
# homeassistant.components.flux_led
|
||||
flux_led==0.28.11
|
||||
flux_led==0.28.17
|
||||
|
||||
# homeassistant.components.homekit
|
||||
fnvhash==0.1.0
|
||||
@@ -764,7 +764,7 @@ google-cloud-pubsub==2.9.0
|
||||
google-cloud-texttospeech==0.4.0
|
||||
|
||||
# homeassistant.components.nest
|
||||
google-nest-sdm==1.5.1
|
||||
google-nest-sdm==1.6.0
|
||||
|
||||
# homeassistant.components.google_travel_time
|
||||
googlemaps==2.5.1
|
||||
@@ -842,7 +842,7 @@ hole==0.7.0
|
||||
holidays==0.12
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20220126.0
|
||||
home-assistant-frontend==20220127.0
|
||||
|
||||
# homeassistant.components.zwave
|
||||
homeassistant-pyozw==0.1.10
|
||||
@@ -1660,7 +1660,7 @@ pymailgunner==1.4
|
||||
pymata-express==1.19
|
||||
|
||||
# homeassistant.components.mazda
|
||||
pymazda==0.3.1
|
||||
pymazda==0.3.2
|
||||
|
||||
# homeassistant.components.mediaroom
|
||||
pymediaroom==0.6.4.1
|
||||
@@ -2087,7 +2087,7 @@ raspyrfm-client==1.2.8
|
||||
regenmaschine==2022.01.0
|
||||
|
||||
# homeassistant.components.renault
|
||||
renault-api==0.1.4
|
||||
renault-api==0.1.7
|
||||
|
||||
# homeassistant.components.python_script
|
||||
restrictedpython==5.2
|
||||
@@ -2111,7 +2111,7 @@ rjpl==0.3.6
|
||||
rocketchat-API==0.6.1
|
||||
|
||||
# homeassistant.components.roku
|
||||
rokuecp==0.11.0
|
||||
rokuecp==0.12.0
|
||||
|
||||
# homeassistant.components.roomba
|
||||
roombapy==1.6.5
|
||||
|
||||
@@ -37,7 +37,7 @@ PyTransportNSW==0.1.1
|
||||
PyTurboJPEG==1.6.5
|
||||
|
||||
# homeassistant.components.vicare
|
||||
PyViCare==2.15.0
|
||||
PyViCare==2.16.1
|
||||
|
||||
# homeassistant.components.xiaomi_aqara
|
||||
PyXiaomiGateway==0.13.4
|
||||
@@ -427,7 +427,7 @@ fjaraskupan==1.0.2
|
||||
flipr-api==1.4.1
|
||||
|
||||
# homeassistant.components.flux_led
|
||||
flux_led==0.28.11
|
||||
flux_led==0.28.17
|
||||
|
||||
# homeassistant.components.homekit
|
||||
fnvhash==0.1.0
|
||||
@@ -492,7 +492,7 @@ google-api-python-client==1.6.4
|
||||
google-cloud-pubsub==2.9.0
|
||||
|
||||
# homeassistant.components.nest
|
||||
google-nest-sdm==1.5.1
|
||||
google-nest-sdm==1.6.0
|
||||
|
||||
# homeassistant.components.google_travel_time
|
||||
googlemaps==2.5.1
|
||||
@@ -543,7 +543,7 @@ hole==0.7.0
|
||||
holidays==0.12
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20220126.0
|
||||
home-assistant-frontend==20220127.0
|
||||
|
||||
# homeassistant.components.zwave
|
||||
homeassistant-pyozw==0.1.10
|
||||
@@ -1041,7 +1041,7 @@ pymailgunner==1.4
|
||||
pymata-express==1.19
|
||||
|
||||
# homeassistant.components.mazda
|
||||
pymazda==0.3.1
|
||||
pymazda==0.3.2
|
||||
|
||||
# homeassistant.components.melcloud
|
||||
pymelcloud==2.5.6
|
||||
@@ -1282,7 +1282,7 @@ rachiopy==1.0.3
|
||||
regenmaschine==2022.01.0
|
||||
|
||||
# homeassistant.components.renault
|
||||
renault-api==0.1.4
|
||||
renault-api==0.1.7
|
||||
|
||||
# homeassistant.components.python_script
|
||||
restrictedpython==5.2
|
||||
@@ -1294,7 +1294,7 @@ rflink==0.0.62
|
||||
ring_doorbell==0.7.2
|
||||
|
||||
# homeassistant.components.roku
|
||||
rokuecp==0.11.0
|
||||
rokuecp==0.12.0
|
||||
|
||||
# homeassistant.components.roomba
|
||||
roombapy==1.6.5
|
||||
|
||||
1
setup.py
1
setup.py
@@ -38,7 +38,6 @@ REQUIRES = [
|
||||
"attrs==21.2.0",
|
||||
"atomicwrites==1.4.0",
|
||||
"awesomeversion==22.1.0",
|
||||
'backports.zoneinfo;python_version<"3.9"',
|
||||
"bcrypt==3.1.7",
|
||||
"certifi>=2021.5.30",
|
||||
"ciso8601==2.2.0",
|
||||
|
||||
@@ -1,8 +1,42 @@
|
||||
"""esphome session fixtures."""
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.esphome import CONF_NOISE_PSK, DOMAIN
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def esphome_mock_async_zeroconf(mock_async_zeroconf):
|
||||
"""Auto mock zeroconf."""
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_config_entry() -> MockConfigEntry:
|
||||
"""Return the default mocked config entry."""
|
||||
return MockConfigEntry(
|
||||
title="ESPHome Device",
|
||||
domain=DOMAIN,
|
||||
data={
|
||||
CONF_HOST: "192.168.1.2",
|
||||
CONF_PORT: 6053,
|
||||
CONF_PASSWORD: "",
|
||||
CONF_NOISE_PSK: "12345678123456781234567812345678",
|
||||
},
|
||||
unique_id="esphome-device",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def init_integration(
|
||||
hass: HomeAssistant, mock_config_entry: MockConfigEntry
|
||||
) -> MockConfigEntry:
|
||||
"""Set up the ESPHome integration for testing."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
return mock_config_entry
|
||||
|
||||
26
tests/components/esphome/test_diagnostics.py
Normal file
26
tests/components/esphome/test_diagnostics.py
Normal file
@@ -0,0 +1,26 @@
|
||||
"""Tests for the diagnostics data provided by the ESPHome integration."""
|
||||
|
||||
from aiohttp import ClientSession
|
||||
|
||||
from homeassistant.components.esphome import CONF_NOISE_PSK
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.components.diagnostics import get_diagnostics_for_config_entry
|
||||
|
||||
|
||||
async def test_diagnostics(
|
||||
hass: HomeAssistant, hass_client: ClientSession, init_integration: MockConfigEntry
|
||||
):
|
||||
"""Test diagnostics for config entry."""
|
||||
result = await get_diagnostics_for_config_entry(hass, hass_client, init_integration)
|
||||
|
||||
assert isinstance(result, dict)
|
||||
assert result["config"]["data"] == {
|
||||
CONF_HOST: "192.168.1.2",
|
||||
CONF_PORT: 6053,
|
||||
CONF_PASSWORD: "**REDACTED**",
|
||||
CONF_NOISE_PSK: "**REDACTED**",
|
||||
}
|
||||
assert result["config"]["unique_id"] == "esphome-device"
|
||||
@@ -1514,3 +1514,34 @@ async def test_query_recover(hass, caplog):
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
async def test_proxy_selected(hass, caplog):
|
||||
"""Test that we handle proxy selected."""
|
||||
|
||||
result = await sh.async_handle_message(
|
||||
hass,
|
||||
BASIC_CONFIG,
|
||||
"test-agent",
|
||||
{
|
||||
"requestId": REQ_ID,
|
||||
"inputs": [
|
||||
{
|
||||
"intent": "action.devices.PROXY_SELECTED",
|
||||
"payload": {
|
||||
"device": {
|
||||
"id": "abcdefg",
|
||||
"customData": {},
|
||||
},
|
||||
"structureData": {},
|
||||
},
|
||||
}
|
||||
],
|
||||
},
|
||||
const.SOURCE_LOCAL,
|
||||
)
|
||||
|
||||
assert result == {
|
||||
"requestId": REQ_ID,
|
||||
"payload": {},
|
||||
}
|
||||
|
||||
@@ -35,16 +35,16 @@ async def test_connectsense_setup(hass):
|
||||
devices=[],
|
||||
entities=[
|
||||
EntityTestInfo(
|
||||
entity_id="sensor.inwall_outlet_0394de_real_time_current",
|
||||
friendly_name="InWall Outlet-0394DE Real Time Current",
|
||||
entity_id="sensor.inwall_outlet_0394de_current",
|
||||
friendly_name="InWall Outlet-0394DE Current",
|
||||
unique_id="homekit-1020301376-aid:1-sid:13-cid:18",
|
||||
capabilities={"state_class": SensorStateClass.MEASUREMENT},
|
||||
unit_of_measurement=ELECTRIC_CURRENT_AMPERE,
|
||||
state="0.03",
|
||||
),
|
||||
EntityTestInfo(
|
||||
entity_id="sensor.inwall_outlet_0394de_real_time_energy",
|
||||
friendly_name="InWall Outlet-0394DE Real Time Energy",
|
||||
entity_id="sensor.inwall_outlet_0394de_power",
|
||||
friendly_name="InWall Outlet-0394DE Power",
|
||||
unique_id="homekit-1020301376-aid:1-sid:13-cid:19",
|
||||
capabilities={"state_class": SensorStateClass.MEASUREMENT},
|
||||
unit_of_measurement=POWER_WATT,
|
||||
@@ -65,16 +65,16 @@ async def test_connectsense_setup(hass):
|
||||
state="on",
|
||||
),
|
||||
EntityTestInfo(
|
||||
entity_id="sensor.inwall_outlet_0394de_real_time_current_2",
|
||||
friendly_name="InWall Outlet-0394DE Real Time Current",
|
||||
entity_id="sensor.inwall_outlet_0394de_current_2",
|
||||
friendly_name="InWall Outlet-0394DE Current",
|
||||
unique_id="homekit-1020301376-aid:1-sid:25-cid:30",
|
||||
capabilities={"state_class": SensorStateClass.MEASUREMENT},
|
||||
unit_of_measurement=ELECTRIC_CURRENT_AMPERE,
|
||||
state="0.05",
|
||||
),
|
||||
EntityTestInfo(
|
||||
entity_id="sensor.inwall_outlet_0394de_real_time_energy_2",
|
||||
friendly_name="InWall Outlet-0394DE Real Time Energy",
|
||||
entity_id="sensor.inwall_outlet_0394de_power_2",
|
||||
friendly_name="InWall Outlet-0394DE Power",
|
||||
unique_id="homekit-1020301376-aid:1-sid:25-cid:31",
|
||||
capabilities={"state_class": SensorStateClass.MEASUREMENT},
|
||||
unit_of_measurement=POWER_WATT,
|
||||
|
||||
@@ -59,9 +59,9 @@ async def test_eve_degree_setup(hass):
|
||||
state="0.400000005960464",
|
||||
),
|
||||
EntityTestInfo(
|
||||
entity_id="sensor.eve_energy_50ff_real_time_energy",
|
||||
entity_id="sensor.eve_energy_50ff_power",
|
||||
unique_id="homekit-AA00A0A00000-aid:1-sid:28-cid:34",
|
||||
friendly_name="Eve Energy 50FF Real Time Energy",
|
||||
friendly_name="Eve Energy 50FF Power",
|
||||
unit_of_measurement=POWER_WATT,
|
||||
capabilities={"state_class": SensorStateClass.MEASUREMENT},
|
||||
state="0",
|
||||
|
||||
@@ -37,8 +37,8 @@ async def test_koogeek_p1eu_setup(hass):
|
||||
state="off",
|
||||
),
|
||||
EntityTestInfo(
|
||||
entity_id="sensor.koogeek_p1_a00aa0_real_time_energy",
|
||||
friendly_name="Koogeek-P1-A00AA0 Real Time Energy",
|
||||
entity_id="sensor.koogeek_p1_a00aa0_power",
|
||||
friendly_name="Koogeek-P1-A00AA0 Power",
|
||||
unique_id="homekit-EUCP03190xxxxx48-aid:1-sid:21-cid:22",
|
||||
unit_of_measurement=POWER_WATT,
|
||||
capabilities={"state_class": SensorStateClass.MEASUREMENT},
|
||||
|
||||
@@ -43,8 +43,8 @@ async def test_koogeek_sw2_setup(hass):
|
||||
state="off",
|
||||
),
|
||||
EntityTestInfo(
|
||||
entity_id="sensor.koogeek_sw2_187a91_real_time_energy",
|
||||
friendly_name="Koogeek-SW2-187A91 Real Time Energy",
|
||||
entity_id="sensor.koogeek_sw2_187a91_power",
|
||||
friendly_name="Koogeek-SW2-187A91 Power",
|
||||
unique_id="homekit-CNNT061751001372-aid:1-sid:14-cid:18",
|
||||
unit_of_measurement=POWER_WATT,
|
||||
capabilities={"state_class": SensorStateClass.MEASUREMENT},
|
||||
|
||||
@@ -37,8 +37,8 @@ async def test_vocolinc_vp3_setup(hass):
|
||||
state="on",
|
||||
),
|
||||
EntityTestInfo(
|
||||
entity_id="sensor.vocolinc_vp3_123456_real_time_energy",
|
||||
friendly_name="VOCOlinc-VP3-123456 Real Time Energy",
|
||||
entity_id="sensor.vocolinc_vp3_123456_power",
|
||||
friendly_name="VOCOlinc-VP3-123456 Power",
|
||||
unique_id="homekit-EU0121203xxxxx07-aid:1-sid:48-cid:97",
|
||||
unit_of_measurement=POWER_WATT,
|
||||
capabilities={"state_class": SensorStateClass.MEASUREMENT},
|
||||
|
||||
@@ -4,8 +4,11 @@ from unittest.mock import patch
|
||||
|
||||
from aiohomekit.model.characteristics import CharacteristicsTypes
|
||||
from aiohomekit.model.services import ServicesTypes
|
||||
from aiohomekit.testing import FakeController
|
||||
|
||||
from homeassistant.components.homekit_controller.const import ENTITY_MAP
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.components.homekit_controller.common import setup_test_component
|
||||
|
||||
@@ -27,3 +30,24 @@ async def test_unload_on_stop(hass, utcnow):
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert async_unlock_mock.called
|
||||
|
||||
|
||||
async def test_async_remove_entry(hass: HomeAssistant):
|
||||
"""Test unpairing a component."""
|
||||
helper = await setup_test_component(hass, create_motion_sensor_service)
|
||||
|
||||
hkid = "00:00:00:00:00:00"
|
||||
|
||||
with patch("aiohomekit.Controller") as controller_cls:
|
||||
# Setup a fake controller with 1 pairing
|
||||
controller = controller_cls.return_value = FakeController()
|
||||
await controller.add_paired_device([helper.accessory], hkid)
|
||||
assert len(controller.pairings) == 1
|
||||
|
||||
assert hkid in hass.data[ENTITY_MAP].storage_data
|
||||
|
||||
# Remove it via config entry and number of pairings should go down
|
||||
await helper.config_entry.async_remove(hass)
|
||||
assert len(controller.pairings) == 0
|
||||
|
||||
assert hkid not in hass.data[ENTITY_MAP].storage_data
|
||||
|
||||
@@ -218,7 +218,7 @@ async def test_switch_with_sensor(hass, utcnow):
|
||||
# Helper will be for the primary entity, which is the outlet. Make a helper for the sensor.
|
||||
energy_helper = Helper(
|
||||
hass,
|
||||
"sensor.testdevice_real_time_energy",
|
||||
"sensor.testdevice_power",
|
||||
helper.pairing,
|
||||
helper.accessory,
|
||||
helper.config_entry,
|
||||
@@ -248,7 +248,7 @@ async def test_sensor_unavailable(hass, utcnow):
|
||||
# Helper will be for the primary entity, which is the outlet. Make a helper for the sensor.
|
||||
energy_helper = Helper(
|
||||
hass,
|
||||
"sensor.testdevice_real_time_energy",
|
||||
"sensor.testdevice_power",
|
||||
helper.pairing,
|
||||
helper.accessory,
|
||||
helper.config_entry,
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
from aiohomekit.model.characteristics import CharacteristicsTypes
|
||||
from aiohomekit.model.services import ServicesTypes
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.homekit_controller import async_remove_entry
|
||||
from homeassistant.components.homekit_controller.const import ENTITY_MAP
|
||||
|
||||
from tests.common import flush_store
|
||||
@@ -79,26 +77,3 @@ async def test_storage_is_updated_on_add(hass, hass_storage, utcnow):
|
||||
# Is saved out to store?
|
||||
await flush_store(entity_map.store)
|
||||
assert hkid in hass_storage[ENTITY_MAP]["data"]["pairings"]
|
||||
|
||||
|
||||
async def test_storage_is_removed_on_config_entry_removal(hass, utcnow):
|
||||
"""Test entity map storage is cleaned up on config entry removal."""
|
||||
await setup_test_component(hass, create_lightbulb_service)
|
||||
|
||||
hkid = "00:00:00:00:00:00"
|
||||
|
||||
pairing_data = {"AccessoryPairingID": hkid}
|
||||
|
||||
entry = config_entries.ConfigEntry(
|
||||
1,
|
||||
"homekit_controller",
|
||||
"TestData",
|
||||
pairing_data,
|
||||
"test",
|
||||
)
|
||||
|
||||
assert hkid in hass.data[ENTITY_MAP].storage_data
|
||||
|
||||
await async_remove_entry(hass, entry)
|
||||
|
||||
assert hkid not in hass.data[ENTITY_MAP].storage_data
|
||||
|
||||
@@ -900,6 +900,15 @@ async def test_get_with_templates(hass, mqtt_mock, caplog):
|
||||
state = hass.states.get(ENTITY_CLIMATE)
|
||||
assert state.attributes.get("hvac_action") == "cooling"
|
||||
|
||||
# Test ignoring null values
|
||||
async_fire_mqtt_message(hass, "action", "null")
|
||||
state = hass.states.get(ENTITY_CLIMATE)
|
||||
assert state.attributes.get("hvac_action") == "cooling"
|
||||
assert (
|
||||
"Invalid ['off', 'heating', 'cooling', 'drying', 'idle', 'fan'] action: None, ignoring"
|
||||
in caplog.text
|
||||
)
|
||||
|
||||
|
||||
async def test_set_with_templates(hass, mqtt_mock, caplog):
|
||||
"""Test setting various attributes with templates."""
|
||||
|
||||
61
tests/components/onewire/test_diagnostics.py
Normal file
61
tests/components/onewire/test_diagnostics.py
Normal file
@@ -0,0 +1,61 @@
|
||||
"""Test 1-Wire diagnostics."""
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.diagnostics import REDACTED
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from . import setup_owproxy_mock_devices
|
||||
|
||||
from tests.components.diagnostics import get_diagnostics_for_config_entry
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def override_platforms():
|
||||
"""Override PLATFORMS."""
|
||||
with patch("homeassistant.components.onewire.PLATFORMS", [Platform.SWITCH]):
|
||||
yield
|
||||
|
||||
|
||||
DEVICE_DETAILS = {
|
||||
"device_info": {
|
||||
"identifiers": [["onewire", "EF.111111111113"]],
|
||||
"manufacturer": "Hobby Boards",
|
||||
"model": "HB_HUB",
|
||||
"name": "EF.111111111113",
|
||||
},
|
||||
"family": "EF",
|
||||
"id": "EF.111111111113",
|
||||
"path": "/EF.111111111113/",
|
||||
"type": "HB_HUB",
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("device_id", ["EF.111111111113"], indirect=True)
|
||||
async def test_entry_diagnostics(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
hass_client,
|
||||
owproxy: MagicMock,
|
||||
device_id: str,
|
||||
):
|
||||
"""Test config entry diagnostics."""
|
||||
setup_owproxy_mock_devices(owproxy, Platform.SENSOR, [device_id])
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == {
|
||||
"entry": {
|
||||
"data": {
|
||||
"host": REDACTED,
|
||||
"port": 1234,
|
||||
"type": "OWServer",
|
||||
},
|
||||
"options": {},
|
||||
"title": "Mock Title",
|
||||
},
|
||||
"devices": [DEVICE_DETAILS],
|
||||
}
|
||||
59
tests/components/p1_monitor/test_diagnostics.py
Normal file
59
tests/components/p1_monitor/test_diagnostics.py
Normal file
@@ -0,0 +1,59 @@
|
||||
"""Tests for the diagnostics data provided by the P1 Monitor integration."""
|
||||
from aiohttp import ClientSession
|
||||
|
||||
from homeassistant.components.diagnostics import REDACTED
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.components.diagnostics import get_diagnostics_for_config_entry
|
||||
|
||||
|
||||
async def test_diagnostics(
|
||||
hass: HomeAssistant,
|
||||
hass_client: ClientSession,
|
||||
init_integration: MockConfigEntry,
|
||||
):
|
||||
"""Test diagnostics."""
|
||||
assert await get_diagnostics_for_config_entry(
|
||||
hass, hass_client, init_integration
|
||||
) == {
|
||||
"entry": {
|
||||
"title": "monitor",
|
||||
"data": {
|
||||
"host": REDACTED,
|
||||
},
|
||||
},
|
||||
"data": {
|
||||
"smartmeter": {
|
||||
"gas_consumption": 2273.447,
|
||||
"energy_tariff_period": "high",
|
||||
"power_consumption": 877,
|
||||
"energy_consumption_high": 2770.133,
|
||||
"energy_consumption_low": 4988.071,
|
||||
"power_production": 0,
|
||||
"energy_production_high": 3971.604,
|
||||
"energy_production_low": 1432.279,
|
||||
},
|
||||
"phases": {
|
||||
"voltage_phase_l1": "233.6",
|
||||
"voltage_phase_l2": "0.0",
|
||||
"voltage_phase_l3": "233.0",
|
||||
"current_phase_l1": "1.6",
|
||||
"current_phase_l2": "4.44",
|
||||
"current_phase_l3": "3.51",
|
||||
"power_consumed_phase_l1": 315,
|
||||
"power_consumed_phase_l2": 0,
|
||||
"power_consumed_phase_l3": 624,
|
||||
"power_produced_phase_l1": 0,
|
||||
"power_produced_phase_l2": 0,
|
||||
"power_produced_phase_l3": 0,
|
||||
},
|
||||
"settings": {
|
||||
"gas_consumption_price": "0.64",
|
||||
"energy_consumption_price_high": "0.20522",
|
||||
"energy_consumption_price_low": "0.20522",
|
||||
"energy_production_price_high": "0.20522",
|
||||
"energy_production_price_low": "0.20522",
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -228,7 +228,7 @@ MOCK_VEHICLES = {
|
||||
},
|
||||
"endpoints_available": [
|
||||
True, # cockpit
|
||||
False, # hvac-status
|
||||
True, # hvac-status
|
||||
True, # location
|
||||
True, # battery-status
|
||||
True, # charge-mode
|
||||
@@ -237,6 +237,7 @@ MOCK_VEHICLES = {
|
||||
"battery_status": "battery_status_not_charging.json",
|
||||
"charge_mode": "charge_mode_schedule.json",
|
||||
"cockpit": "cockpit_ev.json",
|
||||
"hvac_status": "hvac_status.json",
|
||||
"location": "location.json",
|
||||
},
|
||||
Platform.BINARY_SENSOR: [
|
||||
@@ -356,6 +357,14 @@ MOCK_VEHICLES = {
|
||||
ATTR_UNIQUE_ID: "vf1aaaaa555777999_mileage",
|
||||
ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS,
|
||||
},
|
||||
{
|
||||
ATTR_DEVICE_CLASS: SensorDeviceClass.TEMPERATURE,
|
||||
ATTR_ENTITY_ID: "sensor.reg_number_outside_temperature",
|
||||
ATTR_STATE: "8.0",
|
||||
ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT,
|
||||
ATTR_UNIQUE_ID: "vf1aaaaa555777999_outside_temperature",
|
||||
ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS,
|
||||
},
|
||||
{
|
||||
ATTR_DEVICE_CLASS: DEVICE_CLASS_PLUG_STATE,
|
||||
ATTR_ENTITY_ID: "sensor.reg_number_plug_state",
|
||||
|
||||
@@ -476,8 +476,8 @@ async def test_services(
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert mock_roku.play_video.call_count == 1
|
||||
mock_roku.play_video.assert_called_with(
|
||||
assert mock_roku.play_on_roku.call_count == 1
|
||||
mock_roku.play_on_roku.assert_called_with(
|
||||
"https://awesome.tld/media.mp4",
|
||||
{
|
||||
"videoName": "Sent from HA",
|
||||
@@ -496,8 +496,8 @@ async def test_services(
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert mock_roku.play_video.call_count == 2
|
||||
mock_roku.play_video.assert_called_with(
|
||||
assert mock_roku.play_on_roku.call_count == 2
|
||||
mock_roku.play_on_roku.assert_called_with(
|
||||
"https://awesome.tld/api/hls/api_token/master_playlist.m3u8",
|
||||
{
|
||||
"MediaType": "hls",
|
||||
@@ -551,9 +551,9 @@ async def test_services_play_media_local_source(
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert mock_roku.play_video.call_count == 1
|
||||
assert mock_roku.play_video.call_args
|
||||
call_args = mock_roku.play_video.call_args.args
|
||||
assert mock_roku.play_on_roku.call_count == 1
|
||||
assert mock_roku.play_on_roku.call_args
|
||||
call_args = mock_roku.play_on_roku.call_args.args
|
||||
assert "/media/local/Epic%20Sax%20Guy%2010%20Hours.mp4?authSig=" in call_args[0]
|
||||
|
||||
|
||||
|
||||
98
tests/components/rtsp_to_webrtc/conftest.py
Normal file
98
tests/components/rtsp_to_webrtc/conftest.py
Normal file
@@ -0,0 +1,98 @@
|
||||
"""Tests for RTSPtoWebRTC inititalization."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import AsyncGenerator, Awaitable, Callable, Generator
|
||||
from typing import Any, TypeVar
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
import rtsp_to_webrtc
|
||||
|
||||
from homeassistant.components import camera
|
||||
from homeassistant.components.rtsp_to_webrtc import DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
STREAM_SOURCE = "rtsp://example.com"
|
||||
SERVER_URL = "http://127.0.0.1:8083"
|
||||
|
||||
CONFIG_ENTRY_DATA = {"server_url": SERVER_URL}
|
||||
|
||||
# Typing helpers
|
||||
ComponentSetup = Callable[[], Awaitable[None]]
|
||||
T = TypeVar("T")
|
||||
YieldFixture = Generator[T, None, None]
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
async def webrtc_server() -> None:
|
||||
"""Patch client library to force usage of RTSPtoWebRTC server."""
|
||||
with patch(
|
||||
"rtsp_to_webrtc.client.WebClient.heartbeat",
|
||||
side_effect=rtsp_to_webrtc.exceptions.ResponseError(),
|
||||
):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def mock_camera(hass) -> AsyncGenerator[None, None]:
|
||||
"""Initialize a demo camera platform."""
|
||||
assert await async_setup_component(
|
||||
hass, "camera", {camera.DOMAIN: {"platform": "demo"}}
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
with patch(
|
||||
"homeassistant.components.demo.camera.Path.read_bytes",
|
||||
return_value=b"Test",
|
||||
), patch(
|
||||
"homeassistant.components.camera.Camera.stream_source",
|
||||
return_value=STREAM_SOURCE,
|
||||
), patch(
|
||||
"homeassistant.components.camera.Camera.supported_features",
|
||||
return_value=camera.SUPPORT_STREAM,
|
||||
):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def config_entry_data() -> dict[str, Any]:
|
||||
"""Fixture for MockConfigEntry data."""
|
||||
return CONFIG_ENTRY_DATA
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def config_entry(config_entry_data: dict[str, Any]) -> MockConfigEntry:
|
||||
"""Fixture for MockConfigEntry."""
|
||||
return MockConfigEntry(domain=DOMAIN, data=config_entry_data)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def rtsp_to_webrtc_client() -> None:
|
||||
"""Fixture for mock rtsp_to_webrtc client."""
|
||||
with patch("rtsp_to_webrtc.client.Client.heartbeat"):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def setup_integration(
|
||||
hass: HomeAssistant, config_entry: MockConfigEntry
|
||||
) -> YieldFixture[ComponentSetup]:
|
||||
"""Fixture for setting up the component."""
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
async def func() -> None:
|
||||
assert await async_setup_component(hass, DOMAIN, {})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
yield func
|
||||
entries = hass.config_entries.async_entries(DOMAIN)
|
||||
assert len(entries) == 1
|
||||
await hass.config_entries.async_unload(entries[0].entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert not hass.data.get(DOMAIN)
|
||||
assert entries[0].state is ConfigEntryState.NOT_LOADED
|
||||
27
tests/components/rtsp_to_webrtc/test_diagnostics.py
Normal file
27
tests/components/rtsp_to_webrtc/test_diagnostics.py
Normal file
@@ -0,0 +1,27 @@
|
||||
"""Test nest diagnostics."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from .conftest import ComponentSetup
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.components.diagnostics import get_diagnostics_for_config_entry
|
||||
|
||||
THERMOSTAT_TYPE = "sdm.devices.types.THERMOSTAT"
|
||||
|
||||
|
||||
async def test_entry_diagnostics(
|
||||
hass,
|
||||
hass_client,
|
||||
config_entry: MockConfigEntry,
|
||||
rtsp_to_webrtc_client: Any,
|
||||
setup_integration: ComponentSetup,
|
||||
):
|
||||
"""Test config entry diagnostics."""
|
||||
await setup_integration()
|
||||
|
||||
assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == {
|
||||
"discovery": {"attempt": 1, "web.failure": 1, "webrtc.success": 1},
|
||||
"web": {},
|
||||
"webrtc": {},
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
from collections.abc import AsyncGenerator, Awaitable, Callable
|
||||
from collections.abc import Awaitable, Callable
|
||||
from typing import Any
|
||||
from unittest.mock import patch
|
||||
|
||||
@@ -11,147 +11,84 @@ import aiohttp
|
||||
import pytest
|
||||
import rtsp_to_webrtc
|
||||
|
||||
from homeassistant.components import camera
|
||||
from homeassistant.components.rtsp_to_webrtc import DOMAIN
|
||||
from homeassistant.components.websocket_api.const import TYPE_RESULT
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from .conftest import SERVER_URL, STREAM_SOURCE, ComponentSetup
|
||||
|
||||
from tests.test_util.aiohttp import AiohttpClientMocker
|
||||
|
||||
STREAM_SOURCE = "rtsp://example.com"
|
||||
# The webrtc component does not inspect the details of the offer and answer,
|
||||
# and is only a pass through.
|
||||
OFFER_SDP = "v=0\r\no=carol 28908764872 28908764872 IN IP4 100.3.6.6\r\n..."
|
||||
ANSWER_SDP = "v=0\r\no=bob 2890844730 2890844730 IN IP4 host.example.com\r\n..."
|
||||
|
||||
SERVER_URL = "http://127.0.0.1:8083"
|
||||
|
||||
CONFIG_ENTRY_DATA = {"server_url": SERVER_URL}
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
async def webrtc_server() -> None:
|
||||
"""Patch client library to force usage of RTSPtoWebRTC server."""
|
||||
with patch(
|
||||
"rtsp_to_webrtc.client.WebClient.heartbeat",
|
||||
side_effect=rtsp_to_webrtc.exceptions.ResponseError(),
|
||||
):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def mock_camera(hass) -> AsyncGenerator[None, None]:
|
||||
"""Initialize a demo camera platform."""
|
||||
assert await async_setup_component(
|
||||
hass, "camera", {camera.DOMAIN: {"platform": "demo"}}
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
with patch(
|
||||
"homeassistant.components.demo.camera.Path.read_bytes",
|
||||
return_value=b"Test",
|
||||
), patch(
|
||||
"homeassistant.components.camera.Camera.stream_source",
|
||||
return_value=STREAM_SOURCE,
|
||||
), patch(
|
||||
"homeassistant.components.camera.Camera.supported_features",
|
||||
return_value=camera.SUPPORT_STREAM,
|
||||
):
|
||||
yield
|
||||
|
||||
|
||||
async def async_setup_rtsp_to_webrtc(hass: HomeAssistant) -> None:
|
||||
"""Set up the component."""
|
||||
return await async_setup_component(hass, DOMAIN, {})
|
||||
|
||||
|
||||
async def test_setup_success(hass: HomeAssistant) -> None:
|
||||
async def test_setup_success(
|
||||
hass: HomeAssistant, rtsp_to_webrtc_client: Any, setup_integration: ComponentSetup
|
||||
) -> None:
|
||||
"""Test successful setup and unload."""
|
||||
config_entry = MockConfigEntry(domain=DOMAIN, data=CONFIG_ENTRY_DATA)
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
with patch("rtsp_to_webrtc.client.Client.heartbeat"):
|
||||
assert await async_setup_rtsp_to_webrtc(hass)
|
||||
await hass.async_block_till_done()
|
||||
await setup_integration()
|
||||
|
||||
entries = hass.config_entries.async_entries(DOMAIN)
|
||||
assert len(entries) == 1
|
||||
assert entries[0].state is ConfigEntryState.LOADED
|
||||
|
||||
await hass.config_entries.async_unload(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert not hass.data.get(DOMAIN)
|
||||
assert config_entry.state is ConfigEntryState.NOT_LOADED
|
||||
|
||||
|
||||
async def test_invalid_config_entry(hass: HomeAssistant) -> None:
|
||||
@pytest.mark.parametrize("config_entry_data", [{}])
|
||||
async def test_invalid_config_entry(
|
||||
hass: HomeAssistant, rtsp_to_webrtc_client: Any, setup_integration: ComponentSetup
|
||||
) -> None:
|
||||
"""Test a config entry with missing required fields."""
|
||||
config_entry = MockConfigEntry(domain=DOMAIN, data={})
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
assert await async_setup_rtsp_to_webrtc(hass)
|
||||
await setup_integration()
|
||||
|
||||
entries = hass.config_entries.async_entries(DOMAIN)
|
||||
assert len(entries) == 1
|
||||
assert entries[0].state is ConfigEntryState.SETUP_ERROR
|
||||
|
||||
|
||||
async def test_setup_server_failure(hass: HomeAssistant) -> None:
|
||||
async def test_setup_server_failure(
|
||||
hass: HomeAssistant, setup_integration: ComponentSetup
|
||||
) -> None:
|
||||
"""Test server responds with a failure on startup."""
|
||||
config_entry = MockConfigEntry(domain=DOMAIN, data=CONFIG_ENTRY_DATA)
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
with patch(
|
||||
"rtsp_to_webrtc.client.Client.heartbeat",
|
||||
side_effect=rtsp_to_webrtc.exceptions.ResponseError(),
|
||||
):
|
||||
assert await async_setup_rtsp_to_webrtc(hass)
|
||||
await hass.async_block_till_done()
|
||||
await setup_integration()
|
||||
|
||||
entries = hass.config_entries.async_entries(DOMAIN)
|
||||
assert len(entries) == 1
|
||||
assert entries[0].state is ConfigEntryState.SETUP_RETRY
|
||||
|
||||
await hass.config_entries.async_unload(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
||||
async def test_setup_communication_failure(hass: HomeAssistant) -> None:
|
||||
async def test_setup_communication_failure(
|
||||
hass: HomeAssistant, setup_integration: ComponentSetup
|
||||
) -> None:
|
||||
"""Test unable to talk to server on startup."""
|
||||
config_entry = MockConfigEntry(domain=DOMAIN, data=CONFIG_ENTRY_DATA)
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
with patch(
|
||||
"rtsp_to_webrtc.client.Client.heartbeat",
|
||||
side_effect=rtsp_to_webrtc.exceptions.ClientError(),
|
||||
):
|
||||
assert await async_setup_rtsp_to_webrtc(hass)
|
||||
await hass.async_block_till_done()
|
||||
await setup_integration()
|
||||
|
||||
entries = hass.config_entries.async_entries(DOMAIN)
|
||||
assert len(entries) == 1
|
||||
assert entries[0].state is ConfigEntryState.SETUP_RETRY
|
||||
|
||||
await hass.config_entries.async_unload(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
||||
async def test_offer_for_stream_source(
|
||||
hass: HomeAssistant,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
hass_ws_client: Callable[[...], Awaitable[aiohttp.ClientWebSocketResponse]],
|
||||
mock_camera: Any,
|
||||
rtsp_to_webrtc_client: Any,
|
||||
setup_integration: ComponentSetup,
|
||||
) -> None:
|
||||
"""Test successful response from RTSPtoWebRTC server."""
|
||||
config_entry = MockConfigEntry(domain=DOMAIN, data=CONFIG_ENTRY_DATA)
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
with patch("rtsp_to_webrtc.client.Client.heartbeat"):
|
||||
assert await async_setup_rtsp_to_webrtc(hass)
|
||||
await hass.async_block_till_done()
|
||||
await setup_integration()
|
||||
|
||||
aioclient_mock.post(
|
||||
f"{SERVER_URL}/stream",
|
||||
@@ -188,14 +125,11 @@ async def test_offer_failure(
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
hass_ws_client: Callable[[...], Awaitable[aiohttp.ClientWebSocketResponse]],
|
||||
mock_camera: Any,
|
||||
rtsp_to_webrtc_client: Any,
|
||||
setup_integration: ComponentSetup,
|
||||
) -> None:
|
||||
"""Test a transient failure talking to RTSPtoWebRTC server."""
|
||||
config_entry = MockConfigEntry(domain=DOMAIN, data=CONFIG_ENTRY_DATA)
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
with patch("rtsp_to_webrtc.client.Client.heartbeat"):
|
||||
assert await async_setup_rtsp_to_webrtc(hass)
|
||||
await hass.async_block_till_done()
|
||||
await setup_integration()
|
||||
|
||||
aioclient_mock.post(
|
||||
f"{SERVER_URL}/stream",
|
||||
|
||||
@@ -29,25 +29,48 @@ from tests.common import (
|
||||
)
|
||||
|
||||
|
||||
async def test_get_triggers_block_device(hass, coap_wrapper):
|
||||
@pytest.mark.parametrize(
|
||||
"button_type, is_valid",
|
||||
[
|
||||
("momentary", True),
|
||||
("momentary_on_release", True),
|
||||
("detached", True),
|
||||
("toggle", False),
|
||||
],
|
||||
)
|
||||
async def test_get_triggers_block_device(
|
||||
hass, coap_wrapper, monkeypatch, button_type, is_valid
|
||||
):
|
||||
"""Test we get the expected triggers from a shelly block device."""
|
||||
assert coap_wrapper
|
||||
expected_triggers = [
|
||||
{
|
||||
CONF_PLATFORM: "device",
|
||||
CONF_DEVICE_ID: coap_wrapper.device_id,
|
||||
CONF_DOMAIN: DOMAIN,
|
||||
CONF_TYPE: "single",
|
||||
CONF_SUBTYPE: "button1",
|
||||
},
|
||||
{
|
||||
CONF_PLATFORM: "device",
|
||||
CONF_DEVICE_ID: coap_wrapper.device_id,
|
||||
CONF_DOMAIN: DOMAIN,
|
||||
CONF_TYPE: "long",
|
||||
CONF_SUBTYPE: "button1",
|
||||
},
|
||||
]
|
||||
|
||||
monkeypatch.setitem(
|
||||
coap_wrapper.device.settings,
|
||||
"relays",
|
||||
[
|
||||
{"btn_type": button_type},
|
||||
{"btn_type": "toggle"},
|
||||
],
|
||||
)
|
||||
|
||||
expected_triggers = []
|
||||
if is_valid:
|
||||
expected_triggers = [
|
||||
{
|
||||
CONF_PLATFORM: "device",
|
||||
CONF_DEVICE_ID: coap_wrapper.device_id,
|
||||
CONF_DOMAIN: DOMAIN,
|
||||
CONF_TYPE: "single",
|
||||
CONF_SUBTYPE: "button1",
|
||||
},
|
||||
{
|
||||
CONF_PLATFORM: "device",
|
||||
CONF_DEVICE_ID: coap_wrapper.device_id,
|
||||
CONF_DOMAIN: DOMAIN,
|
||||
CONF_TYPE: "long",
|
||||
CONF_SUBTYPE: "button1",
|
||||
},
|
||||
]
|
||||
|
||||
triggers = await async_get_device_automations(
|
||||
hass, DeviceAutomationType.TRIGGER, coap_wrapper.device_id
|
||||
|
||||
@@ -77,15 +77,26 @@ async def test_reconnect_client(hass, aioclient_mock):
|
||||
assert aioclient_mock.call_count == 1
|
||||
|
||||
|
||||
async def test_reconnect_non_existant_device(hass, aioclient_mock):
|
||||
"""Verify no call is made if device does not exist."""
|
||||
await setup_unifi_integration(hass, aioclient_mock)
|
||||
|
||||
aioclient_mock.clear_requests()
|
||||
|
||||
await hass.services.async_call(
|
||||
UNIFI_DOMAIN,
|
||||
SERVICE_RECONNECT_CLIENT,
|
||||
service_data={ATTR_DEVICE_ID: "device_entry.id"},
|
||||
blocking=True,
|
||||
)
|
||||
assert aioclient_mock.call_count == 0
|
||||
|
||||
|
||||
async def test_reconnect_device_without_mac(hass, aioclient_mock):
|
||||
"""Verify no call is made if device does not have a known mac."""
|
||||
config_entry = await setup_unifi_integration(hass, aioclient_mock)
|
||||
controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id]
|
||||
|
||||
aioclient_mock.clear_requests()
|
||||
aioclient_mock.post(
|
||||
f"https://{controller.host}:1234/api/s/{controller.site}/cmd/stamgr",
|
||||
)
|
||||
|
||||
device_registry = await hass.helpers.device_registry.async_get_registry()
|
||||
device_entry = device_registry.async_get_or_create(
|
||||
@@ -139,12 +150,8 @@ async def test_reconnect_client_controller_unavailable(hass, aioclient_mock):
|
||||
async def test_reconnect_client_unknown_mac(hass, aioclient_mock):
|
||||
"""Verify no call is made if trying to reconnect a mac unknown to controller."""
|
||||
config_entry = await setup_unifi_integration(hass, aioclient_mock)
|
||||
controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id]
|
||||
|
||||
aioclient_mock.clear_requests()
|
||||
aioclient_mock.post(
|
||||
f"https://{controller.host}:1234/api/s/{controller.site}/cmd/stamgr",
|
||||
)
|
||||
|
||||
device_registry = await hass.helpers.device_registry.async_get_registry()
|
||||
device_entry = device_registry.async_get_or_create(
|
||||
@@ -172,12 +179,8 @@ async def test_reconnect_wired_client(hass, aioclient_mock):
|
||||
config_entry = await setup_unifi_integration(
|
||||
hass, aioclient_mock, clients_response=clients
|
||||
)
|
||||
controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id]
|
||||
|
||||
aioclient_mock.clear_requests()
|
||||
aioclient_mock.post(
|
||||
f"https://{controller.host}:1234/api/s/{controller.site}/cmd/stamgr",
|
||||
)
|
||||
|
||||
device_registry = await hass.helpers.device_registry.async_get_registry()
|
||||
device_entry = device_registry.async_get_or_create(
|
||||
@@ -264,9 +267,6 @@ async def test_remove_clients_controller_unavailable(hass, aioclient_mock):
|
||||
controller.available = False
|
||||
|
||||
aioclient_mock.clear_requests()
|
||||
aioclient_mock.post(
|
||||
f"https://{controller.host}:1234/api/s/{controller.site}/cmd/stamgr",
|
||||
)
|
||||
|
||||
await hass.services.async_call(UNIFI_DOMAIN, SERVICE_REMOVE_CLIENTS, blocking=True)
|
||||
assert aioclient_mock.call_count == 0
|
||||
@@ -281,15 +281,9 @@ async def test_remove_clients_no_call_on_empty_list(hass, aioclient_mock):
|
||||
"mac": "00:00:00:00:00:01",
|
||||
}
|
||||
]
|
||||
config_entry = await setup_unifi_integration(
|
||||
hass, aioclient_mock, clients_all_response=clients
|
||||
)
|
||||
controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id]
|
||||
await setup_unifi_integration(hass, aioclient_mock, clients_all_response=clients)
|
||||
|
||||
aioclient_mock.clear_requests()
|
||||
aioclient_mock.post(
|
||||
f"https://{controller.host}:1234/api/s/{controller.site}/cmd/stamgr",
|
||||
)
|
||||
|
||||
await hass.services.async_call(UNIFI_DOMAIN, SERVICE_REMOVE_CLIENTS, blocking=True)
|
||||
assert aioclient_mock.call_count == 0
|
||||
|
||||
@@ -216,13 +216,15 @@ class ComponentFactory:
|
||||
|
||||
self._aioclient_mock.clear_requests()
|
||||
self._aioclient_mock.post(
|
||||
"https://account.withings.com/oauth2/token",
|
||||
"https://wbsapi.withings.net/v2/oauth2",
|
||||
json={
|
||||
"refresh_token": "mock-refresh-token",
|
||||
"access_token": "mock-access-token",
|
||||
"type": "Bearer",
|
||||
"expires_in": 60,
|
||||
"userid": profile_config.user_id,
|
||||
"body": {
|
||||
"refresh_token": "mock-refresh-token",
|
||||
"access_token": "mock-access-token",
|
||||
"type": "Bearer",
|
||||
"expires_in": 60,
|
||||
"userid": profile_config.user_id,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -90,13 +90,15 @@ async def test_config_reauth_profile(
|
||||
|
||||
aioclient_mock.clear_requests()
|
||||
aioclient_mock.post(
|
||||
"https://account.withings.com/oauth2/token",
|
||||
"https://wbsapi.withings.net/v2/oauth2",
|
||||
json={
|
||||
"refresh_token": "mock-refresh-token",
|
||||
"access_token": "mock-access-token",
|
||||
"type": "Bearer",
|
||||
"expires_in": 60,
|
||||
"userid": "0",
|
||||
"body": {
|
||||
"refresh_token": "mock-refresh-token",
|
||||
"access_token": "mock-access-token",
|
||||
"type": "Bearer",
|
||||
"expires_in": 60,
|
||||
"userid": "0",
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -512,7 +512,7 @@ async def test_state(hass):
|
||||
"latitude": 32.880837,
|
||||
"longitude": -117.237561,
|
||||
"radius": 250,
|
||||
"passive": True,
|
||||
"passive": False,
|
||||
}
|
||||
assert await setup.async_setup_component(hass, zone.DOMAIN, {"zone": info})
|
||||
|
||||
@@ -521,28 +521,40 @@ async def test_state(hass):
|
||||
assert state.state == "0"
|
||||
|
||||
# Person entity enters zone
|
||||
hass.states.async_set("person.person1", "test_zone")
|
||||
hass.states.async_set(
|
||||
"person.person1",
|
||||
"Test Zone",
|
||||
{"latitude": 32.880837, "longitude": -117.237561, "gps_accuracy": 0},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get("zone.test_zone")
|
||||
assert state.state == "1"
|
||||
assert hass.states.get("zone.test_zone").state == "1"
|
||||
assert hass.states.get("zone.home").state == "0"
|
||||
|
||||
# Person entity enters zone
|
||||
hass.states.async_set("person.person2", "test_zone")
|
||||
hass.states.async_set(
|
||||
"person.person2",
|
||||
"Test Zone",
|
||||
{"latitude": 32.880837, "longitude": -117.237561, "gps_accuracy": 0},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get("zone.test_zone")
|
||||
assert state.state == "2"
|
||||
assert hass.states.get("zone.test_zone").state == "2"
|
||||
assert hass.states.get("zone.home").state == "0"
|
||||
|
||||
# Person entity enters another zone
|
||||
hass.states.async_set("person.person1", "home")
|
||||
hass.states.async_set(
|
||||
"person.person1",
|
||||
"home",
|
||||
{"latitude": 32.87336, "longitude": -117.22743, "gps_accuracy": 0},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get("zone.test_zone")
|
||||
assert state.state == "1"
|
||||
assert hass.states.get("zone.test_zone").state == "1"
|
||||
assert hass.states.get("zone.home").state == "1"
|
||||
|
||||
# Person entity removed
|
||||
hass.states.async_remove("person.person2")
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get("zone.test_zone")
|
||||
assert state.state == "0"
|
||||
assert hass.states.get("zone.test_zone").state == "0"
|
||||
assert hass.states.get("zone.home").state == "1"
|
||||
|
||||
|
||||
async def test_state_2(hass):
|
||||
@@ -555,7 +567,7 @@ async def test_state_2(hass):
|
||||
"latitude": 32.880837,
|
||||
"longitude": -117.237561,
|
||||
"radius": 250,
|
||||
"passive": True,
|
||||
"passive": False,
|
||||
}
|
||||
assert await setup.async_setup_component(hass, zone.DOMAIN, {"zone": info})
|
||||
|
||||
@@ -564,56 +576,37 @@ async def test_state_2(hass):
|
||||
assert state.state == "0"
|
||||
|
||||
# Person entity enters zone
|
||||
hass.states.async_set("person.person1", "test_zone")
|
||||
hass.states.async_set(
|
||||
"person.person1",
|
||||
"Test Zone",
|
||||
{"latitude": 32.880837, "longitude": -117.237561, "gps_accuracy": 0},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get("zone.test_zone")
|
||||
assert state.state == "1"
|
||||
assert hass.states.get("zone.test_zone").state == "1"
|
||||
assert hass.states.get("zone.home").state == "0"
|
||||
|
||||
# Person entity enters zone
|
||||
hass.states.async_set("person.person2", "test_zone")
|
||||
hass.states.async_set(
|
||||
"person.person2",
|
||||
"Test Zone",
|
||||
{"latitude": 32.880837, "longitude": -117.237561, "gps_accuracy": 0},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get("zone.test_zone")
|
||||
assert state.state == "2"
|
||||
assert hass.states.get("zone.test_zone").state == "2"
|
||||
assert hass.states.get("zone.home").state == "0"
|
||||
|
||||
# Person entity enters another zone
|
||||
hass.states.async_set("person.person1", "home")
|
||||
hass.states.async_set(
|
||||
"person.person1",
|
||||
"home",
|
||||
{"latitude": 32.87336, "longitude": -117.22743, "gps_accuracy": 0},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get("zone.test_zone")
|
||||
assert state.state == "1"
|
||||
assert hass.states.get("zone.test_zone").state == "1"
|
||||
assert hass.states.get("zone.home").state == "1"
|
||||
|
||||
# Person entity removed
|
||||
hass.states.async_remove("person.person2")
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get("zone.test_zone")
|
||||
assert state.state == "0"
|
||||
|
||||
|
||||
async def test_state_3(hass):
|
||||
"""Test the state of a zone."""
|
||||
hass.states.async_set("person.person1", "test_zone")
|
||||
hass.states.async_set("person.person2", "test_zone")
|
||||
|
||||
info = {
|
||||
"name": "Test Zone",
|
||||
"latitude": 32.880837,
|
||||
"longitude": -117.237561,
|
||||
"radius": 250,
|
||||
"passive": True,
|
||||
}
|
||||
assert await setup.async_setup_component(hass, zone.DOMAIN, {"zone": info})
|
||||
|
||||
assert len(hass.states.async_entity_ids("zone")) == 2
|
||||
state = hass.states.get("zone.test_zone")
|
||||
assert state.state == "2"
|
||||
|
||||
# Person entity enters another zone
|
||||
hass.states.async_set("person.person1", "home")
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get("zone.test_zone")
|
||||
assert state.state == "1"
|
||||
|
||||
# Person entity removed
|
||||
hass.states.async_remove("person.person2")
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get("zone.test_zone")
|
||||
assert state.state == "0"
|
||||
assert hass.states.get("zone.test_zone").state == "0"
|
||||
assert hass.states.get("zone.home").state == "1"
|
||||
|
||||
Reference in New Issue
Block a user