forked from home-assistant/core
Compare commits
55 Commits
2022.2.0b0
...
2022.2.0b3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd6c182c07 | ||
|
|
f8e0c41e91 | ||
|
|
5f56107116 | ||
|
|
fb3c99a891 | ||
|
|
ca505b79b5 | ||
|
|
c74a8bf65a | ||
|
|
406801ef73 | ||
|
|
2bfedcbdc5 | ||
|
|
84f817eb25 | ||
|
|
4ead2f2f7e | ||
|
|
421f9716a7 | ||
|
|
25e6d8858c | ||
|
|
3829a81d15 | ||
|
|
9318843867 | ||
|
|
4eb787b619 | ||
|
|
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 |
6
.github/workflows/builder.yml
vendored
6
.github/workflows/builder.yml
vendored
@@ -76,8 +76,10 @@ jobs:
|
||||
- name: Build package
|
||||
shell: bash
|
||||
run: |
|
||||
pip install twine wheel
|
||||
python setup.py sdist bdist_wheel
|
||||
# Remove dist, build, and homeassistant.egg-info
|
||||
# when build locally for testing!
|
||||
pip install twine build
|
||||
python -m build
|
||||
|
||||
- name: Upload package
|
||||
shell: bash
|
||||
|
||||
@@ -107,7 +107,7 @@ repos:
|
||||
pass_filenames: false
|
||||
language: script
|
||||
types: [text]
|
||||
files: ^(homeassistant/.+/manifest\.json|setup\.py|\.pre-commit-config\.yaml|script/gen_requirements_all\.py)$
|
||||
files: ^(homeassistant/.+/manifest\.json|setup\.cfg|\.pre-commit-config\.yaml|script/gen_requirements_all\.py)$
|
||||
- id: hassfest
|
||||
name: hassfest
|
||||
entry: script/run-in-env.sh python3 -m script.hassfest
|
||||
@@ -115,3 +115,10 @@ repos:
|
||||
language: script
|
||||
types: [text]
|
||||
files: ^(homeassistant/.+/(manifest|strings)\.json|\.coveragerc|\.strict-typing|homeassistant/.+/services\.yaml|script/hassfest/.+\.py)$
|
||||
- id: hassfest-metadata
|
||||
name: hassfest-metadata
|
||||
entry: script/run-in-env.sh python3 -m script.hassfest -p metadata
|
||||
pass_filenames: false
|
||||
language: script
|
||||
types: [text]
|
||||
files: ^(script/hassfest/.+\.py|homeassistant/const\.py$|setup\.cfg)$
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
include README.rst
|
||||
include LICENSE.md
|
||||
graft homeassistant
|
||||
recursive-exclude * *.py[co]
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -8,7 +8,11 @@
|
||||
{
|
||||
"hostname": "blink*",
|
||||
"macaddress": "B85F98*"
|
||||
}
|
||||
},
|
||||
{
|
||||
"hostname": "blink*",
|
||||
"macaddress": "00037F*"
|
||||
}
|
||||
],
|
||||
"config_flow": true,
|
||||
"iot_class": "cloud_polling"
|
||||
|
||||
@@ -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,
|
||||
@@ -342,14 +343,15 @@ class FritzBoxTools(update_coordinator.DataUpdateCoordinator):
|
||||
|
||||
for interf in node["node_interfaces"]:
|
||||
dev_mac = interf["mac_address"]
|
||||
|
||||
if dev_mac not in hosts:
|
||||
continue
|
||||
|
||||
dev_info: Device = hosts[dev_mac]
|
||||
|
||||
for link in interf["node_links"]:
|
||||
intf = mesh_intf.get(link["node_interface_1_uid"])
|
||||
if (
|
||||
intf is not None
|
||||
and link["state"] == "CONNECTED"
|
||||
and dev_mac in hosts
|
||||
):
|
||||
dev_info: Device = hosts[dev_mac]
|
||||
if intf is not None:
|
||||
if intf["op_mode"] != "AP_GUEST":
|
||||
dev_info.wan_access = not self.connection.call_action(
|
||||
"X_AVM-DE_HostFilter:1",
|
||||
@@ -360,14 +362,15 @@ class FritzBoxTools(update_coordinator.DataUpdateCoordinator):
|
||||
dev_info.connected_to = intf["device"]
|
||||
dev_info.connection_type = intf["type"]
|
||||
dev_info.ssid = intf.get("ssid")
|
||||
_LOGGER.debug("Client dev_info: %s", dev_info)
|
||||
|
||||
if dev_mac in self._devices:
|
||||
self._devices[dev_mac].update(dev_info, consider_home)
|
||||
else:
|
||||
device = FritzDevice(dev_mac, dev_info.name)
|
||||
device.update(dev_info, consider_home)
|
||||
self._devices[dev_mac] = device
|
||||
new_device = True
|
||||
if dev_mac in self._devices:
|
||||
self._devices[dev_mac].update(dev_info, consider_home)
|
||||
else:
|
||||
device = FritzDevice(dev_mac, dev_info.name)
|
||||
device.update(dev_info, consider_home)
|
||||
self._devices[dev_mac] = device
|
||||
new_device = True
|
||||
|
||||
dispatcher_send(self.hass, self.signal_device_update)
|
||||
if new_device:
|
||||
@@ -523,6 +526,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%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,6 +158,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
existing_entry = await self.async_set_unique_id(isy_mac)
|
||||
if not existing_entry:
|
||||
return
|
||||
if existing_entry.source == config_entries.SOURCE_IGNORE:
|
||||
raise data_entry_flow.AbortFlow("already_configured")
|
||||
parsed_url = urlparse(existing_entry.data[CONF_HOST])
|
||||
if parsed_url.hostname != ip_address:
|
||||
new_netloc = ip_address
|
||||
|
||||
@@ -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,6 +4,7 @@ from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from google_nest_sdm import diagnostics
|
||||
from google_nest_sdm.device import Device
|
||||
from google_nest_sdm.device_traits import InfoTrait
|
||||
from google_nest_sdm.exceptions import ApiException
|
||||
@@ -30,22 +31,14 @@ async def async_get_config_entry_diagnostics(
|
||||
return {"error": str(err)}
|
||||
|
||||
return {
|
||||
**diagnostics.get_diagnostics(),
|
||||
"devices": [
|
||||
get_device_data(device) for device in device_manager.devices.values()
|
||||
]
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def get_device_data(device: Device) -> dict[str, Any]:
|
||||
"""Return diagnostic information about a device."""
|
||||
# Return a simplified view of the API object, but skipping any id fields or
|
||||
# traits that include unique identifiers or personally identifiable information.
|
||||
# See https://developers.google.com/nest/device-access/traits for API details
|
||||
return {
|
||||
"type": device.type,
|
||||
"traits": {
|
||||
trait: data
|
||||
for trait, data in device.raw_data.get("traits", {}).items()
|
||||
if trait not in REDACT_DEVICE_TRAITS
|
||||
},
|
||||
}
|
||||
# Library performs its own redaction for device data
|
||||
return device.get_diagnostics()
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
"domain": "oncue",
|
||||
"name": "Oncue by Kohler",
|
||||
"config_flow": true,
|
||||
"dhcp": [{
|
||||
"hostname": "kohlergen*",
|
||||
"macaddress": "00146F*"
|
||||
}],
|
||||
"documentation": "https://www.home-assistant.io/integrations/oncue",
|
||||
"requirements": ["aiooncue==0.3.2"],
|
||||
"codeowners": ["@bdraco"],
|
||||
|
||||
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()
|
||||
|
||||
|
||||
@@ -13,7 +13,11 @@
|
||||
{
|
||||
"hostname": "roomba-*",
|
||||
"macaddress": "80A589*"
|
||||
}
|
||||
},
|
||||
{
|
||||
"hostname": "roomba-*",
|
||||
"macaddress": "DCF505*"
|
||||
}
|
||||
],
|
||||
"iot_class": "local_push"
|
||||
}
|
||||
|
||||
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())
|
||||
@@ -5,6 +5,7 @@ from abc import ABC, abstractmethod
|
||||
import contextlib
|
||||
from typing import Any
|
||||
|
||||
from requests.exceptions import Timeout as RequestsTimeout
|
||||
from samsungctl import Remote
|
||||
from samsungctl.exceptions import AccessDenied, ConnectionClosed, UnhandledResponse
|
||||
from samsungtvws import SamsungTVWS
|
||||
@@ -321,7 +322,7 @@ class SamsungTVWSBridge(SamsungTVBridge):
|
||||
def device_info(self) -> dict[str, Any] | None:
|
||||
"""Try to gather infos of this TV."""
|
||||
if remote := self._get_remote(avoid_open=True):
|
||||
with contextlib.suppress(HttpApiError):
|
||||
with contextlib.suppress(HttpApiError, RequestsTimeout):
|
||||
device_info: dict[str, Any] = remote.rest_device_info()
|
||||
return device_info
|
||||
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -25,6 +25,10 @@
|
||||
"hostname": "k[lp]*",
|
||||
"macaddress": "403F8C*"
|
||||
},
|
||||
{
|
||||
"hostname": "k[lp]*",
|
||||
"macaddress": "C0C9E3*"
|
||||
},
|
||||
{
|
||||
"hostname": "ep*",
|
||||
"macaddress": "E848B8*"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -137,7 +137,7 @@ class TuyaFanEntity(TuyaEntity, FanEntity):
|
||||
[
|
||||
{
|
||||
"code": self._speed.dpcode,
|
||||
"value": self._speed.scale_value_back(percentage),
|
||||
"value": int(self._speed.remap_value_from(percentage, 0, 100)),
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
@@ -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 = "0b3"
|
||||
__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)
|
||||
|
||||
@@ -46,6 +46,11 @@ DHCP = [
|
||||
"hostname": "blink*",
|
||||
"macaddress": "B85F98*"
|
||||
},
|
||||
{
|
||||
"domain": "blink",
|
||||
"hostname": "blink*",
|
||||
"macaddress": "00037F*"
|
||||
},
|
||||
{
|
||||
"domain": "broadlink",
|
||||
"macaddress": "34EA34*"
|
||||
@@ -201,6 +206,11 @@ DHCP = [
|
||||
"domain": "nuki",
|
||||
"hostname": "nuki_bridge_*"
|
||||
},
|
||||
{
|
||||
"domain": "oncue",
|
||||
"hostname": "kohlergen*",
|
||||
"macaddress": "00146F*"
|
||||
},
|
||||
{
|
||||
"domain": "overkiz",
|
||||
"hostname": "gateway*",
|
||||
@@ -250,6 +260,11 @@ DHCP = [
|
||||
"hostname": "roomba-*",
|
||||
"macaddress": "80A589*"
|
||||
},
|
||||
{
|
||||
"domain": "roomba",
|
||||
"hostname": "roomba-*",
|
||||
"macaddress": "DCF505*"
|
||||
},
|
||||
{
|
||||
"domain": "samsungtv",
|
||||
"hostname": "tizen*"
|
||||
@@ -392,6 +407,11 @@ DHCP = [
|
||||
"hostname": "k[lp]*",
|
||||
"macaddress": "403F8C*"
|
||||
},
|
||||
{
|
||||
"domain": "tplink",
|
||||
"hostname": "k[lp]*",
|
||||
"macaddress": "C0C9E3*"
|
||||
},
|
||||
{
|
||||
"domain": "tplink",
|
||||
"hostname": "ep*",
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
[build-system]
|
||||
requires = ["setuptools~=60.5", "wheel~=0.37.1"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[tool.black]
|
||||
target-version = ["py38"]
|
||||
exclude = 'generated'
|
||||
|
||||
@@ -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,5 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate an updated requirements_all.txt."""
|
||||
import configparser
|
||||
import difflib
|
||||
import importlib
|
||||
import os
|
||||
@@ -167,10 +168,9 @@ def explore_module(package, explore_children):
|
||||
|
||||
def core_requirements():
|
||||
"""Gather core requirements out of setup.py."""
|
||||
reqs_raw = re.search(
|
||||
r"REQUIRES = \[(.*?)\]", Path("setup.py").read_text(), re.S
|
||||
).group(1)
|
||||
return [x[1] for x in re.findall(r"(['\"])(.*?)\1", reqs_raw)]
|
||||
parser = configparser.ConfigParser()
|
||||
parser.read("setup.cfg")
|
||||
return parser["options"]["install_requires"].strip().split("\n")
|
||||
|
||||
|
||||
def gather_recursive_requirements(domain, seen=None):
|
||||
|
||||
@@ -12,6 +12,7 @@ from . import (
|
||||
dhcp,
|
||||
json,
|
||||
manifest,
|
||||
metadata,
|
||||
mqtt,
|
||||
mypy_config,
|
||||
requirements,
|
||||
@@ -41,6 +42,7 @@ INTEGRATION_PLUGINS = [
|
||||
HASS_PLUGINS = [
|
||||
coverage,
|
||||
mypy_config,
|
||||
metadata,
|
||||
]
|
||||
|
||||
|
||||
|
||||
31
script/hassfest/metadata.py
Normal file
31
script/hassfest/metadata.py
Normal file
@@ -0,0 +1,31 @@
|
||||
"""Package metadata validation."""
|
||||
import configparser
|
||||
|
||||
from homeassistant.const import REQUIRED_PYTHON_VER, __version__
|
||||
|
||||
from .model import Config, Integration
|
||||
|
||||
|
||||
def validate(integrations: dict[str, Integration], config: Config) -> None:
|
||||
"""Validate project metadata keys."""
|
||||
metadata_path = config.root / "setup.cfg"
|
||||
parser = configparser.ConfigParser()
|
||||
parser.read(metadata_path)
|
||||
|
||||
try:
|
||||
if parser["metadata"]["version"] != __version__:
|
||||
config.add_error(
|
||||
"metadata", f"'metadata.version' value does not match '{__version__}'"
|
||||
)
|
||||
except KeyError:
|
||||
config.add_error("metadata", "No 'metadata.version' key found!")
|
||||
|
||||
required_py_version = f">={'.'.join(map(str, REQUIRED_PYTHON_VER))}"
|
||||
try:
|
||||
if parser["options"]["python_requires"] != required_py_version:
|
||||
config.add_error(
|
||||
"metadata",
|
||||
f"'options.python_requires' value doesn't match '{required_py_version}",
|
||||
)
|
||||
except KeyError:
|
||||
config.add_error("metadata", "No 'options.python_requires' key found!")
|
||||
@@ -1,32 +0,0 @@
|
||||
#!/bin/sh
|
||||
# Pushes a new version to PyPi.
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
head -n 5 homeassistant/const.py | tail -n 1 | grep PATCH_VERSION > /dev/null
|
||||
|
||||
if [ $? -eq 1 ]
|
||||
then
|
||||
echo "Patch version not found on const.py line 5"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
head -n 5 homeassistant/const.py | tail -n 1 | grep dev > /dev/null
|
||||
|
||||
if [ $? -eq 0 ]
|
||||
then
|
||||
echo "Release version should not contain dev tag"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
CURRENT_BRANCH=`git rev-parse --abbrev-ref HEAD`
|
||||
|
||||
if [ "$CURRENT_BRANCH" != "master" ] && [ "$CURRENT_BRANCH" != "rc" ]
|
||||
then
|
||||
echo "You have to be on the master or rc branch to release."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
rm -rf dist build
|
||||
python3 setup.py sdist bdist_wheel
|
||||
python3 -m twine upload dist/* --skip-existing
|
||||
@@ -117,7 +117,18 @@ def write_version(version):
|
||||
)
|
||||
|
||||
with open("homeassistant/const.py", "wt") as fil:
|
||||
content = fil.write(content)
|
||||
fil.write(content)
|
||||
|
||||
|
||||
def write_version_metadata(version: Version) -> None:
|
||||
"""Update setup.cfg file with new version."""
|
||||
with open("setup.cfg") as fp:
|
||||
content = fp.read()
|
||||
|
||||
content = re.sub(r"(version\W+=\W).+\n", f"\\g<1>{version}\n", content, count=1)
|
||||
|
||||
with open("setup.cfg", "w") as fp:
|
||||
fp.write(content)
|
||||
|
||||
|
||||
def main():
|
||||
@@ -142,6 +153,7 @@ def main():
|
||||
assert bumped > current, "BUG! New version is not newer than old version"
|
||||
|
||||
write_version(bumped)
|
||||
write_version_metadata(bumped)
|
||||
|
||||
if not arguments.commit:
|
||||
return
|
||||
|
||||
53
setup.cfg
53
setup.cfg
@@ -1,10 +1,21 @@
|
||||
[metadata]
|
||||
name = homeassistant
|
||||
version = 2022.2.0b3
|
||||
author = The Home Assistant Authors
|
||||
author_email = hello@home-assistant.io
|
||||
license = Apache-2.0
|
||||
license_file = LICENSE.md
|
||||
platforms = any
|
||||
description = Open-source home automation platform running on Python 3.
|
||||
long_description = file: README.rst
|
||||
long_description_content_type = text/x-rst
|
||||
keywords = home, automation
|
||||
url = https://www.home-assistant.io/
|
||||
project_urls =
|
||||
Source Code = https://github.com/home-assistant/core
|
||||
Bug Reports = https://github.com/home-assistant/core/issues
|
||||
Docs: Dev = https://developers.home-assistant.io/
|
||||
Discord = https://discordapp.com/invite/c5DvZ4e
|
||||
Forum = https://community.home-assistant.io/
|
||||
classifier =
|
||||
Development Status :: 4 - Beta
|
||||
Intended Audience :: End Users/Desktop
|
||||
@@ -14,6 +25,46 @@ classifier =
|
||||
Programming Language :: Python :: 3.9
|
||||
Topic :: Home Automation
|
||||
|
||||
[options]
|
||||
packages = find:
|
||||
zip_safe = False
|
||||
include_package_data = True
|
||||
python_requires = >=3.9.0
|
||||
install_requires =
|
||||
aiohttp==3.8.1
|
||||
astral==2.2
|
||||
async_timeout==4.0.2
|
||||
attrs==21.2.0
|
||||
atomicwrites==1.4.0
|
||||
awesomeversion==22.1.0
|
||||
bcrypt==3.1.7
|
||||
certifi>=2021.5.30
|
||||
ciso8601==2.2.0
|
||||
# When bumping httpx, please check the version pins of
|
||||
# httpcore, anyio, and h11 in gen_requirements_all
|
||||
httpx==0.21.3
|
||||
ifaddr==0.1.7
|
||||
jinja2==3.0.3
|
||||
PyJWT==2.1.0
|
||||
# PyJWT has loose dependency. We want the latest one.
|
||||
cryptography==35.0.0
|
||||
pip>=8.0.3,<20.3
|
||||
python-slugify==4.0.1
|
||||
pyyaml==6.0
|
||||
requests==2.27.1
|
||||
typing-extensions>=3.10.0.2,<5.0
|
||||
voluptuous==0.12.2
|
||||
voluptuous-serialize==2.5.0
|
||||
yarl==1.7.2
|
||||
|
||||
[options.packages.find]
|
||||
include =
|
||||
homeassistant*
|
||||
|
||||
[options.entry_points]
|
||||
console_scripts =
|
||||
hass = homeassistant.__main__:main
|
||||
|
||||
[flake8]
|
||||
exclude = .venv,.git,.tox,docs,venv,bin,lib,deps,build
|
||||
max-complexity = 25
|
||||
|
||||
85
setup.py
Executable file → Normal file
85
setup.py
Executable file → Normal file
@@ -1,80 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Home Assistant setup script."""
|
||||
from datetime import datetime as dt
|
||||
"""
|
||||
Entry point for setuptools. Required for editable installs.
|
||||
TODO: Remove file after updating to pip 21.3
|
||||
"""
|
||||
from setuptools import setup
|
||||
|
||||
from setuptools import find_packages, setup
|
||||
|
||||
import homeassistant.const as hass_const
|
||||
|
||||
PROJECT_NAME = "Home Assistant"
|
||||
PROJECT_PACKAGE_NAME = "homeassistant"
|
||||
PROJECT_LICENSE = "Apache License 2.0"
|
||||
PROJECT_AUTHOR = "The Home Assistant Authors"
|
||||
PROJECT_COPYRIGHT = f" 2013-{dt.now().year}, {PROJECT_AUTHOR}"
|
||||
PROJECT_URL = "https://www.home-assistant.io/"
|
||||
PROJECT_EMAIL = "hello@home-assistant.io"
|
||||
|
||||
PROJECT_GITHUB_USERNAME = "home-assistant"
|
||||
PROJECT_GITHUB_REPOSITORY = "core"
|
||||
|
||||
PYPI_URL = f"https://pypi.python.org/pypi/{PROJECT_PACKAGE_NAME}"
|
||||
GITHUB_PATH = f"{PROJECT_GITHUB_USERNAME}/{PROJECT_GITHUB_REPOSITORY}"
|
||||
GITHUB_URL = f"https://github.com/{GITHUB_PATH}"
|
||||
|
||||
DOWNLOAD_URL = f"{GITHUB_URL}/archive/{hass_const.__version__}.zip"
|
||||
PROJECT_URLS = {
|
||||
"Bug Reports": f"{GITHUB_URL}/issues",
|
||||
"Dev Docs": "https://developers.home-assistant.io/",
|
||||
"Discord": "https://discordapp.com/invite/c5DvZ4e",
|
||||
"Forum": "https://community.home-assistant.io/",
|
||||
}
|
||||
|
||||
PACKAGES = find_packages(exclude=["tests", "tests.*"])
|
||||
|
||||
REQUIRES = [
|
||||
"aiohttp==3.8.1",
|
||||
"astral==2.2",
|
||||
"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",
|
||||
# When bumping httpx, please check the version pins of
|
||||
# httpcore, anyio, and h11 in gen_requirements_all
|
||||
"httpx==0.21.3",
|
||||
"ifaddr==0.1.7",
|
||||
"jinja2==3.0.3",
|
||||
"PyJWT==2.1.0",
|
||||
# PyJWT has loose dependency. We want the latest one.
|
||||
"cryptography==35.0.0",
|
||||
"pip>=8.0.3,<20.3",
|
||||
"python-slugify==4.0.1",
|
||||
"pyyaml==6.0",
|
||||
"requests==2.27.1",
|
||||
"typing-extensions>=3.10.0.2,<5.0",
|
||||
"voluptuous==0.12.2",
|
||||
"voluptuous-serialize==2.5.0",
|
||||
"yarl==1.7.2",
|
||||
]
|
||||
|
||||
MIN_PY_VERSION = ".".join(map(str, hass_const.REQUIRED_PYTHON_VER))
|
||||
|
||||
setup(
|
||||
name=PROJECT_PACKAGE_NAME,
|
||||
version=hass_const.__version__,
|
||||
url=PROJECT_URL,
|
||||
download_url=DOWNLOAD_URL,
|
||||
project_urls=PROJECT_URLS,
|
||||
author=PROJECT_AUTHOR,
|
||||
author_email=PROJECT_EMAIL,
|
||||
packages=PACKAGES,
|
||||
include_package_data=True,
|
||||
zip_safe=False,
|
||||
install_requires=REQUIRES,
|
||||
python_requires=f">={MIN_PY_VERSION}",
|
||||
test_suite="tests",
|
||||
entry_points={"console_scripts": ["hass = homeassistant.__main__:main"]},
|
||||
)
|
||||
setup()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -16,7 +16,12 @@ from homeassistant.components.isy994.const import (
|
||||
ISY_URL_POSTFIX,
|
||||
UDN_UUID_PREFIX,
|
||||
)
|
||||
from homeassistant.config_entries import SOURCE_DHCP, SOURCE_IMPORT, SOURCE_SSDP
|
||||
from homeassistant.config_entries import (
|
||||
SOURCE_DHCP,
|
||||
SOURCE_IGNORE,
|
||||
SOURCE_IMPORT,
|
||||
SOURCE_SSDP,
|
||||
)
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
@@ -595,3 +600,27 @@ async def test_form_dhcp_existing_entry_preserves_port(hass: HomeAssistant):
|
||||
assert result["reason"] == "already_configured"
|
||||
assert entry.data[CONF_HOST] == f"http://1.2.3.4:1443{ISY_URL_POSTFIX}"
|
||||
assert entry.data[CONF_USERNAME] == "bob"
|
||||
|
||||
|
||||
async def test_form_dhcp_existing_ignored_entry(hass: HomeAssistant):
|
||||
"""Test we handled an ignored entry from dhcp."""
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN, data={}, unique_id=MOCK_UUID, source=SOURCE_IGNORE
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
with patch(PATCH_CONNECTION, return_value=MOCK_CONFIG_RESPONSE):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_DHCP},
|
||||
data=dhcp.DhcpServiceInfo(
|
||||
ip="1.2.3.4",
|
||||
hostname="isy994-ems",
|
||||
macaddress=MOCK_MAC,
|
||||
),
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Common libraries for test setup."""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Generator
|
||||
import copy
|
||||
import shutil
|
||||
from typing import Any
|
||||
@@ -8,6 +9,7 @@ from unittest.mock import patch
|
||||
import uuid
|
||||
|
||||
import aiohttp
|
||||
from google_nest_sdm import diagnostics
|
||||
from google_nest_sdm.auth import AbstractAuth
|
||||
from google_nest_sdm.device_manager import DeviceManager
|
||||
import pytest
|
||||
@@ -234,3 +236,10 @@ async def setup_platform(
|
||||
) -> PlatformSetup:
|
||||
"""Fixture to setup the integration platform and subscriber."""
|
||||
return setup_base_platform
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def reset_diagnostics() -> Generator[None, None, None]:
|
||||
"""Fixture to reset client library diagnostic counters."""
|
||||
yield
|
||||
diagnostics.reset()
|
||||
|
||||
@@ -56,13 +56,21 @@ async def test_entry_diagnostics(hass, hass_client):
|
||||
assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == {
|
||||
"devices": [
|
||||
{
|
||||
"traits": {
|
||||
"sdm.devices.traits.Humidity": {"ambientHumidityPercent": 35.0},
|
||||
"sdm.devices.traits.Temperature": {
|
||||
"ambientTemperatureCelsius": 25.1
|
||||
"data": {
|
||||
"assignee": "**REDACTED**",
|
||||
"name": "**REDACTED**",
|
||||
"parentRelations": [
|
||||
{"displayName": "**REDACTED**", "parent": "**REDACTED**"}
|
||||
],
|
||||
"traits": {
|
||||
"sdm.devices.traits.Info": {"customName": "**REDACTED**"},
|
||||
"sdm.devices.traits.Humidity": {"ambientHumidityPercent": 35.0},
|
||||
"sdm.devices.traits.Temperature": {
|
||||
"ambientTemperatureCelsius": 25.1
|
||||
},
|
||||
},
|
||||
},
|
||||
"type": "sdm.devices.types.THERMOSTAT",
|
||||
"type": "sdm.devices.types.THERMOSTAT",
|
||||
}
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
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