Compare commits

...

40 Commits

Author SHA1 Message Date
Paulus Schoutsen
8e38b7624e Bumped version to 2022.2.0b2 2022-01-28 09:37:50 -08:00
Allen Porter
fdb52df7b7 Add diagnostics for rtsp_to_webrtc (#65138) 2022-01-28 09:37:32 -08:00
Allen Porter
6c3e8b06ea Bump google-nest-sdm to 1.6.0 (diagnostics) (#65135) 2022-01-28 09:37:31 -08:00
Nenad Bogojevic
6ba52b1c86 Use new withings oauth2 refresh token endpoint (#65134) 2022-01-28 09:37:30 -08:00
epenet
1e60958fc4 Add diagnostics support to onewire (#65131)
* Add diagnostics support to onewire

* Add tests

Co-authored-by: epenet <epenet@users.noreply.github.com>
2022-01-28 09:37:30 -08:00
Simone Chemelli
0f9e65e687 Handle FritzInternalError exception for Fritz (#65124) 2022-01-28 09:37:29 -08:00
starkillerOG
d382e24e5b Goodwe - fix value errors (#65121) 2022-01-28 09:37:28 -08:00
Erik Montnemery
82acaa380c Fix cast support for browsing local media source (#65115) 2022-01-28 09:37:27 -08:00
Hans Oischinger
0a00177a8f Handle vicare I/O in executor (#65105)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2022-01-28 09:37:27 -08:00
J. Nick Koston
34cf82b017 Downgrade homekit linked humidity sensor error to debug (#65098)
Fixes #65015
2022-01-28 09:37:26 -08:00
Shay Levy
44403dab62 Fix Shelly 1/1PM external temperature sensor unavailable (#65096) 2022-01-28 09:37:25 -08:00
Paulus Schoutsen
909b0fb689 Add support for proxy-selected intent (#65094) 2022-01-28 09:37:25 -08:00
Robert Svensson
3f763ddc9a Reconnect client service tried to connect even if device didn't exist (#65082) 2022-01-28 09:37:24 -08:00
G Johansson
837d49f67b Fix Yale optionsflow (#65072) 2022-01-28 09:37:23 -08:00
Jc2k
735edd83fc Support unpairing homekit accessories from homekit_controller (#65065) 2022-01-28 09:37:22 -08:00
Klaas Schoute
7415513352 Add diagnostics support to P1 Monitor (#65060)
* Add diagnostics to P1 Monitor

* Add test for diagnostics
2022-01-28 09:37:22 -08:00
Shay Levy
6f20a75583 Fix Shelly detached switches automation triggers (#65059) 2022-01-28 09:37:21 -08:00
Jc2k
05d7fef9f0 Better names for energy related homekit_controller sensors (#65055) 2022-01-28 09:37:20 -08:00
Thibaut
2ff8f10b9f Check explicitly for None value in Overkiz integration (#65045) 2022-01-28 09:37:19 -08:00
Paulus Schoutsen
0604185854 Bumped version to 2022.2.0b1 2022-01-27 11:24:06 -08:00
epenet
ff445b69f4 Update Renault to 0.1.7 (#65076)
* Update Renault to 0.1.7

* Adjust tests accordingly

Co-authored-by: epenet <epenet@users.noreply.github.com>
2022-01-27 11:23:38 -08:00
Paulus Schoutsen
7e2d04ca77 Bump frontend to 20220127.0 (#65075) 2022-01-27 11:23:37 -08:00
jjlawren
07d2627dc5 Guard browsing Spotify if setup failed (#65074)
Co-authored-by: Franck Nijhof <frenck@frenck.nl>
2022-01-27 11:23:37 -08:00
Hans Oischinger
1968ddb3fd Update PyVicare to 2.16.1 (#65073) 2022-01-27 11:23:36 -08:00
Marc Mueller
5a90f106d1 Remove backports.zoneinfo dependency (#65069) 2022-01-27 11:23:35 -08:00
Martin Hjelmare
8afb0aa44a Fix notify leaving zone blueprint (#65056) 2022-01-27 11:23:34 -08:00
Simon Hansen
6f8b0a01b4 Fix typo in entity name for launchlibrary (#65048) 2022-01-27 11:23:34 -08:00
Duco Sebel
035b589fca Add flow_title for HomeWizard Energy (#65047) 2022-01-27 11:23:33 -08:00
Erik Montnemery
3e94d39c64 Unset Alexa authorized flag in additional case (#65044) 2022-01-27 11:23:32 -08:00
Erik Montnemery
a768de51c0 Correct zone state (#65040)
Co-authored-by: Franck Nijhof <git@frenck.dev>
2022-01-27 11:23:32 -08:00
Shay Levy
25ffda7cd4 Fix Shelly battery powered devices unavailable (#65031) 2022-01-27 11:23:31 -08:00
Chris Talkington
290a0df2be Update rokuecp to 0.12.0 (#65030) 2022-01-27 11:23:30 -08:00
Paulus Schoutsen
662ec1377a Catch connection reset error (#65027) 2022-01-27 11:23:30 -08:00
Brandon Rothweiler
057f1a701f Bump pymazda to 0.3.2 (#65025) 2022-01-27 11:23:29 -08:00
Joakim Sørensen
03e369dc86 Set ping data to None instead of False (#65013) 2022-01-27 11:23:28 -08:00
J. Nick Koston
c831270262 Bump flux_led to fix push updates on newer devices (#65011) 2022-01-27 11:23:27 -08:00
Franck Nijhof
9eb18564b7 Handle Tuya sendings strings instead of numeric values (#65009) 2022-01-27 11:23:27 -08:00
Jesse Hills
a7d83993be Add diagnostics download to ESPHome (#65008) 2022-01-27 11:23:26 -08:00
Arjan van Balken
25ea728f21 Update Arris TG2492LG dependency to 1.2.1 (#64999) 2022-01-27 11:23:26 -08:00
Jan Bouwhuis
63048a67e0 Fix MQTT climate action null warnings (#64658) 2022-01-27 11:23:25 -08:00
77 changed files with 944 additions and 375 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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 [],
}

View File

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

View File

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

View File

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

View 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__,
},
}

View File

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

View File

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

View File

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

View File

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

View 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())

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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],
}

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

View File

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

View File

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

View 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

View 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": {},
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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