Compare commits

...

34 Commits

Author SHA1 Message Date
Paulus Schoutsen
919f4dd719 Merge pull request #69509 from home-assistant/rc 2022-04-07 23:10:23 -07:00
Allen Porter
d9cbbd3b05 Fix bugs calendar oauth token date handling (#69641) 2022-04-07 21:53:56 -07:00
Matt Zimmerman
7e317bed3e [powerwall] Skip backup reserve sensor if data is unavailable (#69637) 2022-04-07 20:34:25 -07:00
David F. Mulcahey
8017cb274e Fix Samjin Multi acceleration in ZHA (#69636) 2022-04-07 20:28:15 -07:00
David F. Mulcahey
4d4eb5c850 Bump ZHA quirks to 0.0.71 (#69633) 2022-04-07 17:16:51 -07:00
puddly
1866e58ac5 Move new zha_event command parameters into a params key to ensure backwards compatibility (#69631) 2022-04-07 15:33:50 -07:00
north3221
b50a78d1d9 Fix tado default overlay for when set pre new overlay feature (#69584) 2022-04-07 15:23:25 -07:00
puddly
88a081be24 Fix ZHA group creation (#69629) 2022-04-07 15:05:11 -07:00
J. Nick Koston
3dd0ddb73e Mark backgrounds optional for tplink random effects (#69622) 2022-04-07 15:05:10 -07:00
Álvaro Fernández Rojas
9063428358 Update aioairzone to v0.3.3 (#69615) 2022-04-07 15:05:09 -07:00
Álvaro Fernández Rojas
ee06b2a1b5 Update aioairzone to v0.3.1 (#68975) 2022-04-07 15:05:08 -07:00
Diogo Gomes
62d67a4287 Fix utility_meter reset service (#69612) 2022-04-07 15:02:49 -07:00
Jason Hunter
0b2f0a9f7c Log which device has the time discrepancy (#69595) 2022-04-07 15:02:49 -07:00
Dave T
7803845af1 Generic fix stream thumbnail (#69378) 2022-04-07 15:02:48 -07:00
J. Nick Koston
2dd3dc2d2d Run energy db calls in the db executor (#69544)
Fixes #69537
2022-04-07 15:26:15 +02:00
J. Nick Koston
ceb8d86a7e Fix registered entities without a category not being exclude-able in the HomeKit UI (#69543) 2022-04-07 15:26:12 +02:00
Joakim Sørensen
e726ef662c Fix adding OS entities for supervised installations (#69539) 2022-04-07 15:26:08 +02:00
Allen Porter
8c9534d2ba Gracefully handle empty summary in google calendar (#69520)
Gracefully handle empty summary in google calendar matching the old behavior
before some code cleanup.
2022-04-07 15:26:04 +02:00
Paulus Schoutsen
5cadea91bb Bumped version to 2022.4.1 2022-04-06 22:36:39 -07:00
J. Nick Koston
f9d447e4cd Fix reloading the sun integration (#69495) 2022-04-06 22:35:55 -07:00
Shay Levy
23bb38c5cf Fix remote_rpi_gpio missing requirement (#69488) 2022-04-06 22:35:55 -07:00
Joakim Sørensen
4c16563675 Bump pyhaversion from 22.04.0 to 22.4.1 (#69486) 2022-04-06 22:35:54 -07:00
J. Nick Koston
9351fcf369 Fix reload race in unifiprotect (#69485)
- The integration already has a reload listener installed
  once it is setup. We should not reload from the config
  flow since they compete
2022-04-06 22:35:53 -07:00
Michael
2d74beaa67 Ignore IPv6 link local address on ssdp discovery in Fritz!Smarthome (#69455) 2022-04-06 22:35:52 -07:00
J. Nick Koston
87ab96f9c1 Fix elkm1 connection when panel drops VN request (#69454) 2022-04-06 22:35:52 -07:00
Paulus Schoutsen
0eed329bc8 Fix telegram broadcast (#69452) 2022-04-06 22:35:51 -07:00
Dave T
ea5e894ac7 Continue on template error during yaml import for generic (#69440)
Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
2022-04-06 22:35:50 -07:00
Raman Gupta
91d2fafe1d Add comments to zwave_js node metadata WS API (#67210)
* Add comments to zwave_js node metadata WS API

* Add test dat
2022-04-06 22:35:50 -07:00
Franck Nijhof
7dd19066e8 Merge pull request #69413 from home-assistant/rc 2022-04-06 15:14:55 +02:00
Franck Nijhof
be3c1055dd Bumped version to 2022.4.0 2022-04-06 14:01:53 +02:00
René Klomp
5a24dbbbf2 Update pysma to 0.6.11 (#69397) 2022-04-06 14:00:49 +02:00
Erik Montnemery
8174b831cf Restore attributes of template binary sensor (#69350) 2022-04-06 14:00:46 +02:00
Raman Gupta
8c794ecf93 Fix regression in zwave_js (#69312)
* Handle unique ID update during discovery step

* Use callback to convert unique IDs to strings

* Adjust test to make sure logic works

* Fix other tests

* Move comment

* Move migration to async_setup

* Remove async_migrate_entry since we take care of it during setup

* Remove unused test
2022-04-06 14:00:42 +02:00
hesselonline
072cd29b90 Fix Wallbox charger status (#68708)
Co-authored-by: Erik Montnemery <erik@montnemery.com>
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2022-04-06 14:00:31 +02:00
74 changed files with 846 additions and 234 deletions

View File

@@ -3,7 +3,7 @@
"name": "Airzone",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/airzone",
"requirements": ["aioairzone==0.2.3"],
"requirements": ["aioairzone==0.3.3"],
"codeowners": ["@Noltari"],
"iot_class": "local_polling",
"loggers": ["aioairzone"]

View File

@@ -363,6 +363,9 @@ async def async_wait_for_elk_to_sync(
# VN is the first command sent for panel, when we get
# it back we now we are logged in either with or without a password
elk.add_handler("VN", first_response)
# Some panels do not respond to the vn request so we
# check for lw as well
elk.add_handler("LW", first_response)
elk.add_handler("sync_complete", sync_complete)
for name, event, timeout in (
("login", login_event, login_timeout),

View File

@@ -489,7 +489,7 @@ async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation:
# Fetch the needed statistics metadata
statistics_metadata.update(
await hass.async_add_executor_job(
await recorder.get_instance(hass).async_add_executor_job(
functools.partial(
recorder.statistics.get_metadata,
hass,

View File

@@ -260,7 +260,7 @@ async def ws_get_fossil_energy_consumption(
statistic_ids.append(msg["co2_statistic_id"])
# Fetch energy + CO2 statistics
statistics = await hass.async_add_executor_job(
statistics = await recorder.get_instance(hass).async_add_executor_job(
recorder.statistics.statistics_during_period,
hass,
start_time,

View File

@@ -1,6 +1,7 @@
"""Config flow for AVM FRITZ!SmartHome."""
from __future__ import annotations
import ipaddress
from typing import Any
from urllib.parse import urlparse
@@ -120,6 +121,12 @@ class FritzboxConfigFlow(ConfigFlow, domain=DOMAIN):
assert isinstance(host, str)
self.context[CONF_HOST] = host
if (
ipaddress.ip_address(host).version == 6
and ipaddress.ip_address(host).is_link_local
):
return self.async_abort(reason="ignore_ip6_link_local")
if uuid := discovery_info.upnp.get(ssdp.ATTR_UPNP_UDN):
if uuid.startswith("uuid:"):
uuid = uuid[5:]

View File

@@ -28,6 +28,7 @@
"abort": {
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"ignore_ip6_link_local": "IPv6 link local address is not supported.",
"no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]",
"not_supported": "Connected to AVM FRITZ!Box but it's unable to control Smart Home devices.",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"

View File

@@ -3,6 +3,7 @@
"abort": {
"already_configured": "Device is already configured",
"already_in_progress": "Configuration flow is already in progress",
"ignore_ip6_link_local": "IPv6 link local address is not supported.",
"no_devices_found": "No devices found on the network",
"not_supported": "Connected to AVM FRITZ!Box but it's unable to control Smart Home devices.",
"reauth_successful": "Re-authentication was successful"

View File

@@ -109,6 +109,20 @@ def build_schema(
return vol.Schema(spec)
def build_schema_content_type(user_input: dict[str, Any] | MappingProxyType[str, Any]):
"""Create schema for conditional 2nd page specifying stream content_type."""
return vol.Schema(
{
vol.Required(
CONF_CONTENT_TYPE,
description={
"suggested_value": user_input.get(CONF_CONTENT_TYPE, "image/jpeg")
},
): str,
}
)
def get_image_type(image):
"""Get the format of downloaded bytes that could be an image."""
fmt = None
@@ -129,14 +143,14 @@ async def async_test_still(hass, info) -> tuple[dict[str, str], str | None]:
"""Verify that the still image is valid before we create an entity."""
fmt = None
if not (url := info.get(CONF_STILL_IMAGE_URL)):
return {}, None
return {}, info.get(CONF_CONTENT_TYPE, "image/jpeg")
if not isinstance(url, template_helper.Template) and url:
url = cv.template(url)
url.hass = hass
try:
url = url.async_render(parse_result=False)
except TemplateError as err:
_LOGGER.error("Error parsing template %s: %s", url, err)
_LOGGER.warning("Problem rendering template %s: %s", url, err)
return {CONF_STILL_IMAGE_URL: "template_error"}, None
verify_ssl = info.get(CONF_VERIFY_SSL)
auth = generate_auth(info)
@@ -228,6 +242,11 @@ class GenericIPCamConfigFlow(ConfigFlow, domain=DOMAIN):
VERSION = 1
def __init__(self):
"""Initialize Generic ConfigFlow."""
self.cached_user_input: dict[str, Any] = {}
self.cached_title = ""
@staticmethod
def async_get_options_flow(
config_entry: ConfigEntry,
@@ -238,8 +257,8 @@ class GenericIPCamConfigFlow(ConfigFlow, domain=DOMAIN):
def check_for_existing(self, options):
"""Check whether an existing entry is using the same URLs."""
return any(
entry.options[CONF_STILL_IMAGE_URL] == options[CONF_STILL_IMAGE_URL]
and entry.options[CONF_STREAM_SOURCE] == options[CONF_STREAM_SOURCE]
entry.options.get(CONF_STILL_IMAGE_URL) == options.get(CONF_STILL_IMAGE_URL)
and entry.options.get(CONF_STREAM_SOURCE) == options.get(CONF_STREAM_SOURCE)
for entry in self._async_current_entries()
)
@@ -264,10 +283,17 @@ class GenericIPCamConfigFlow(ConfigFlow, domain=DOMAIN):
if not errors:
user_input[CONF_CONTENT_TYPE] = still_format
user_input[CONF_LIMIT_REFETCH_TO_URL_CHANGE] = False
await self.async_set_unique_id(self.flow_id)
return self.async_create_entry(
title=name, data={}, options=user_input
)
if user_input.get(CONF_STILL_IMAGE_URL):
await self.async_set_unique_id(self.flow_id)
return self.async_create_entry(
title=name, data={}, options=user_input
)
# If user didn't specify a still image URL,
# we can't (yet) autodetect it from the stream.
# Show a conditional 2nd page to ask them the content type.
self.cached_user_input = user_input
self.cached_title = name
return await self.async_step_content_type()
else:
user_input = DEFAULT_DATA.copy()
@@ -277,12 +303,36 @@ class GenericIPCamConfigFlow(ConfigFlow, domain=DOMAIN):
errors=errors,
)
async def async_step_content_type(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle the user's choice for stream content_type."""
if user_input is not None:
user_input = self.cached_user_input | user_input
await self.async_set_unique_id(self.flow_id)
return self.async_create_entry(
title=self.cached_title, data={}, options=user_input
)
return self.async_show_form(
step_id="content_type",
data_schema=build_schema_content_type({}),
errors={},
)
async def async_step_import(self, import_config) -> FlowResult:
"""Handle config import from yaml."""
# abort if we've already got this one.
if self.check_for_existing(import_config):
return self.async_abort(reason="already_exists")
errors, still_format = await async_test_still(self.hass, import_config)
if errors.get(CONF_STILL_IMAGE_URL) == "template_error":
_LOGGER.warning(
"Could not render template, but it could be that "
"referenced entities are still initialising. "
"Continuing assuming that imported YAML template is valid"
)
errors.pop(CONF_STILL_IMAGE_URL)
still_format = import_config.get(CONF_CONTENT_TYPE, "image/jpeg")
errors = errors | await async_test_stream(self.hass, import_config)
still_url = import_config.get(CONF_STILL_IMAGE_URL)
stream_url = import_config.get(CONF_STREAM_SOURCE)
@@ -308,6 +358,8 @@ class GenericOptionsFlowHandler(OptionsFlow):
def __init__(self, config_entry: ConfigEntry) -> None:
"""Initialize Generic IP Camera options flow."""
self.config_entry = config_entry
self.cached_user_input: dict[str, Any] = {}
self.cached_title = ""
async def async_step_init(
self, user_input: dict[str, Any] | None = None
@@ -316,29 +368,52 @@ class GenericOptionsFlowHandler(OptionsFlow):
errors: dict[str, str] = {}
if user_input is not None:
errors, still_format = await async_test_still(self.hass, user_input)
errors, still_format = await async_test_still(
self.hass, self.config_entry.options | user_input
)
errors = errors | await async_test_stream(self.hass, user_input)
still_url = user_input.get(CONF_STILL_IMAGE_URL)
stream_url = user_input.get(CONF_STREAM_SOURCE)
if not errors:
return self.async_create_entry(
title=slug_url(still_url) or slug_url(stream_url) or DEFAULT_NAME,
data={
CONF_AUTHENTICATION: user_input.get(CONF_AUTHENTICATION),
CONF_STREAM_SOURCE: user_input.get(CONF_STREAM_SOURCE),
CONF_PASSWORD: user_input.get(CONF_PASSWORD),
CONF_STILL_IMAGE_URL: user_input.get(CONF_STILL_IMAGE_URL),
CONF_CONTENT_TYPE: still_format,
CONF_USERNAME: user_input.get(CONF_USERNAME),
CONF_LIMIT_REFETCH_TO_URL_CHANGE: user_input[
CONF_LIMIT_REFETCH_TO_URL_CHANGE
],
CONF_FRAMERATE: user_input[CONF_FRAMERATE],
CONF_VERIFY_SSL: user_input[CONF_VERIFY_SSL],
},
)
title = slug_url(still_url) or slug_url(stream_url) or DEFAULT_NAME
data = {
CONF_AUTHENTICATION: user_input.get(CONF_AUTHENTICATION),
CONF_STREAM_SOURCE: user_input.get(CONF_STREAM_SOURCE),
CONF_PASSWORD: user_input.get(CONF_PASSWORD),
CONF_STILL_IMAGE_URL: user_input.get(CONF_STILL_IMAGE_URL),
CONF_CONTENT_TYPE: still_format
or self.config_entry.options.get(CONF_CONTENT_TYPE),
CONF_USERNAME: user_input.get(CONF_USERNAME),
CONF_LIMIT_REFETCH_TO_URL_CHANGE: user_input[
CONF_LIMIT_REFETCH_TO_URL_CHANGE
],
CONF_FRAMERATE: user_input[CONF_FRAMERATE],
CONF_VERIFY_SSL: user_input[CONF_VERIFY_SSL],
}
if still_url:
return self.async_create_entry(
title=title,
data=data,
)
self.cached_title = title
self.cached_user_input = data
return await self.async_step_content_type()
return self.async_show_form(
step_id="init",
data_schema=build_schema(user_input or self.config_entry.options, True),
errors=errors,
)
async def async_step_content_type(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle the user's choice for stream content_type."""
if user_input is not None:
user_input = self.cached_user_input | user_input
return self.async_create_entry(title=self.cached_title, data=user_input)
return self.async_show_form(
step_id="content_type",
data_schema=build_schema_content_type(self.cached_user_input),
errors={},
)

View File

@@ -30,11 +30,16 @@
"limit_refetch_to_url_change": "Limit refetch to url change",
"password": "[%key:common::config_flow::data::password%]",
"username": "[%key:common::config_flow::data::username%]",
"content_type": "Content Type",
"framerate": "Frame Rate (Hz)",
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
}
},
"content_type": {
"description": "Specify the content type for the stream.",
"data": {
"content_type": "Content Type"
}
},
"confirm": {
"description": "[%key:common::config_flow::description::confirm_setup%]"
}
@@ -51,10 +56,15 @@
"limit_refetch_to_url_change": "[%key:component::generic::config::step::user::data::limit_refetch_to_url_change%]",
"password": "[%key:common::config_flow::data::password%]",
"username": "[%key:common::config_flow::data::username%]",
"content_type": "[%key:component::generic::config::step::user::data::content_type%]",
"framerate": "[%key:component::generic::config::step::user::data::framerate%]",
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
}
},
"content_type": {
"description": "[%key:component::generic::config::step::content_type::description%]",
"data": {
"content_type": "[%key:component::generic::config::step::content_type::data::content_type%]"
}
}
},
"error": {

View File

@@ -23,10 +23,15 @@
"confirm": {
"description": "Do you want to start set up?"
},
"content_type": {
"data": {
"content_type": "Content Type"
},
"description": "Specify the content type for the stream."
},
"user": {
"data": {
"authentication": "Authentication",
"content_type": "Content Type",
"framerate": "Frame Rate (Hz)",
"limit_refetch_to_url_change": "Limit refetch to url change",
"password": "Password",
@@ -57,10 +62,15 @@
"unknown": "Unexpected error"
},
"step": {
"content_type": {
"data": {
"content_type": "Content Type"
},
"description": "Specify the content type for the stream."
},
"init": {
"data": {
"authentication": "Authentication",
"content_type": "Content Type",
"framerate": "Frame Rate (Hz)",
"limit_refetch_to_url_change": "Limit refetch to url change",
"password": "Password",

View File

@@ -5,6 +5,7 @@ from __future__ import annotations
from collections.abc import Awaitable, Callable
import datetime
import logging
import time
from typing import Any
from googleapiclient import discovery as google_discovery
@@ -58,7 +59,7 @@ class DeviceAuth(config_entry_oauth2_flow.LocalOAuth2Implementation):
"refresh_token": creds.refresh_token,
"scope": " ".join(creds.scopes),
"token_type": "Bearer",
"expires_in": creds.token_expiry.timestamp(),
"expires_in": creds.token_expiry.timestamp() - time.time(),
}
@@ -157,16 +158,16 @@ def _async_google_creds(hass: HomeAssistant, token: dict[str, Any]) -> Credentia
client_id=conf[CONF_CLIENT_ID],
client_secret=conf[CONF_CLIENT_SECRET],
refresh_token=token["refresh_token"],
token_expiry=token["expires_at"],
token_expiry=datetime.datetime.fromtimestamp(token["expires_at"]),
token_uri=oauth2client.GOOGLE_TOKEN_URI,
scopes=[conf[CONF_CALENDAR_ACCESS].scope],
user_agent=None,
)
def _api_time_format(time: datetime.datetime | None) -> str | None:
def _api_time_format(date_time: datetime.datetime | None) -> str | None:
"""Convert a datetime to the api string format."""
return time.isoformat("T") if time else None
return date_time.isoformat("T") if date_time else None
class GoogleCalendarService:

View File

@@ -183,7 +183,9 @@ class GoogleCalendarEventDevice(CalendarEventDevice):
valid_items = filter(self._event_filter, items)
self._event = copy.deepcopy(next(valid_items, None))
if self._event:
(summary, offset) = extract_offset(self._event["summary"], self._offset)
(summary, offset) = extract_offset(
self._event.get("summary", ""), self._offset
)
self._event["summary"] = summary
self._offset_reached = is_offset_reached(
get_date(self._event["start"]), offset

View File

@@ -824,7 +824,7 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator):
self.data = {}
self.entry_id = config_entry.entry_id
self.dev_reg = dev_reg
self.is_hass_os = "hassos" in get_info(self.hass)
self.is_hass_os = (get_info(self.hass) or {}).get("hassos") is not None
async def _async_update_data(self) -> dict[str, Any]:
"""Update data via library."""
@@ -891,6 +891,12 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator):
if stale_addons := supervisor_addon_devices - set(new_data[DATA_KEY_ADDONS]):
async_remove_addons_from_dev_reg(self.dev_reg, stale_addons)
if not self.is_hass_os and (
dev := self.dev_reg.async_get_device({(DOMAIN, "OS")})
):
# Remove the OS device if it exists and the installation is not hassos
self.dev_reg.async_remove_device(dev.id)
# If there are new add-ons, we should reload the config entry so we can
# create new devices and entities. We can return an empty dict because
# coordinator will be recreated.

View File

@@ -652,7 +652,7 @@ def _exclude_by_entity_registry(
(entry := ent_reg.async_get(entity_id))
and (
entry.hidden_by is not None
or (not include_entity_category or entry.entity_category is not None)
or (not include_entity_category and entry.entity_category is not None)
)
)

View File

@@ -204,9 +204,10 @@ class ONVIFDevice:
if self._dt_diff_seconds > 5:
LOGGER.warning(
"The date/time on the device (UTC) is '%s', "
"The date/time on %s (UTC) is '%s', "
"which is different from the system '%s', "
"this could lead to authentication issues",
self.name,
cam_date_utc,
system_date,
)

View File

@@ -223,13 +223,18 @@ def call_base_info(power_wall: Powerwall, host: str) -> PowerwallBaseInfo:
def _fetch_powerwall_data(power_wall: Powerwall) -> PowerwallData:
"""Process and update powerwall data."""
try:
backup_reserve = power_wall.get_backup_reserve_percentage()
except MissingAttributeError:
backup_reserve = None
return PowerwallData(
charge=power_wall.get_charge(),
site_master=power_wall.get_sitemaster(),
meters=power_wall.get_meters(),
grid_services_active=power_wall.is_grid_services_active(),
grid_status=power_wall.get_grid_status(),
backup_reserve=power_wall.get_backup_reserve_percentage(),
backup_reserve=backup_reserve,
)

View File

@@ -38,7 +38,7 @@ class PowerwallData:
meters: MetersAggregates
grid_services_active: bool
grid_status: GridStatus
backup_reserve: float
backup_reserve: float | None
class PowerwallRuntimeData(TypedDict):

View File

@@ -117,9 +117,11 @@ async def async_setup_entry(
data: PowerwallData = coordinator.data
entities: list[PowerWallEntity] = [
PowerWallChargeSensor(powerwall_data),
PowerWallBackupReserveSensor(powerwall_data),
]
if data.backup_reserve is not None:
entities.append(PowerWallBackupReserveSensor(powerwall_data))
for meter in data.meters.meters:
entities.append(PowerWallExportSensor(powerwall_data, meter))
entities.append(PowerWallImportSensor(powerwall_data, meter))
@@ -190,8 +192,10 @@ class PowerWallBackupReserveSensor(PowerWallEntity, SensorEntity):
return f"{self.base_unique_id}_backup_reserve"
@property
def native_value(self) -> int:
def native_value(self) -> int | None:
"""Get the current value in percentage."""
if self.data.backup_reserve is None:
return None
return round(self.data.backup_reserve)

View File

@@ -2,8 +2,8 @@
"domain": "remote_rpi_gpio",
"name": "remote_rpi_gpio",
"documentation": "https://www.home-assistant.io/integrations/remote_rpi_gpio",
"requirements": ["gpiozero==1.5.1"],
"requirements": ["gpiozero==1.6.2", "pigpio==1.78"],
"codeowners": [],
"iot_class": "local_push",
"loggers": ["gpiozero"]
"loggers": ["gpiozero", "pigpio"]
}

View File

@@ -3,7 +3,7 @@
"name": "SMA Solar",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/sma",
"requirements": ["pysma==0.6.10"],
"requirements": ["pysma==0.6.11"],
"codeowners": ["@kellerza", "@rklomp"],
"iot_class": "local_polling",
"loggers": ["pysma"]

View File

@@ -130,9 +130,12 @@ class Sun(Entity):
self._config_listener = self.hass.bus.async_listen(
EVENT_CORE_CONFIG_UPDATE, self.update_location
)
self._loaded_listener = self.hass.bus.async_listen(
EVENT_COMPONENT_LOADED, self.loading_complete
)
if DOMAIN in hass.config.components:
self.update_location()
else:
self._loaded_listener = self.hass.bus.async_listen(
EVENT_COMPONENT_LOADED, self.loading_complete
)
@callback
def loading_complete(self, event_: Event) -> None:
@@ -158,6 +161,7 @@ class Sun(Entity):
"""Remove the loaded listener."""
if self._loaded_listener:
self._loaded_listener()
self._loaded_listener = None
@callback
def remove_listeners(self):

View File

@@ -18,7 +18,10 @@ from homeassistant.util import Throttle
from .const import (
CONF_FALLBACK,
CONST_OVERLAY_MANUAL,
CONST_OVERLAY_TADO_DEFAULT,
CONST_OVERLAY_TADO_MODE,
CONST_OVERLAY_TADO_OPTIONS,
DATA,
DOMAIN,
INSIDE_TEMPERATURE_MEASUREMENT,
@@ -51,7 +54,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
username = entry.data[CONF_USERNAME]
password = entry.data[CONF_PASSWORD]
fallback = entry.options.get(CONF_FALLBACK, CONST_OVERLAY_TADO_MODE)
fallback = entry.options.get(CONF_FALLBACK, CONST_OVERLAY_TADO_DEFAULT)
tadoconnector = TadoConnector(hass, username, password, fallback)
@@ -99,7 +102,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
def _async_import_options_from_data_if_missing(hass: HomeAssistant, entry: ConfigEntry):
options = dict(entry.options)
if CONF_FALLBACK not in options:
options[CONF_FALLBACK] = entry.data.get(CONF_FALLBACK, CONST_OVERLAY_TADO_MODE)
options[CONF_FALLBACK] = entry.data.get(
CONF_FALLBACK, CONST_OVERLAY_TADO_DEFAULT
)
hass.config_entries.async_update_entry(entry, options=options)
if options[CONF_FALLBACK] not in CONST_OVERLAY_TADO_OPTIONS:
if options[CONF_FALLBACK]:
options[CONF_FALLBACK] = CONST_OVERLAY_TADO_MODE
else:
options[CONF_FALLBACK] = CONST_OVERLAY_MANUAL
hass.config_entries.async_update_entry(entry, options=options)

View File

@@ -11,7 +11,13 @@ from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import callback
from homeassistant.data_entry_flow import FlowResult
from .const import CONF_FALLBACK, CONST_OVERLAY_TADO_OPTIONS, DOMAIN, UNIQUE_ID
from .const import (
CONF_FALLBACK,
CONST_OVERLAY_TADO_DEFAULT,
CONST_OVERLAY_TADO_OPTIONS,
DOMAIN,
UNIQUE_ID,
)
_LOGGER = logging.getLogger(__name__)
@@ -126,7 +132,10 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
data_schema = vol.Schema(
{
vol.Optional(
CONF_FALLBACK, default=self.config_entry.options.get(CONF_FALLBACK)
CONF_FALLBACK,
default=self.config_entry.options.get(
CONF_FALLBACK, CONST_OVERLAY_TADO_DEFAULT
),
): vol.In(CONST_OVERLAY_TADO_OPTIONS),
}
)

View File

@@ -1,17 +1,6 @@
"""Support for Telegram bot to send messages only."""
import logging
from . import initialize_bot
_LOGGER = logging.getLogger(__name__)
async def async_setup_platform(hass, config):
async def async_setup_platform(hass, bot, config):
"""Set up the Telegram broadcast platform."""
bot = initialize_bot(config)
bot_config = await hass.async_add_executor_job(bot.getMe)
_LOGGER.debug(
"Telegram broadcast platform setup with bot %s", bot_config["username"]
)
return True

View File

@@ -331,6 +331,7 @@ class TriggerBinarySensorEntity(TriggerEntity, BinarySensorEntity, RestoreEntity
and self._state is None
):
self._state = last_state.state == STATE_ON
self.restore_attributes(last_state)
if CONF_AUTO_OFF not in self._config:
return

View File

@@ -4,8 +4,16 @@ from __future__ import annotations
import logging
from typing import Any
from homeassistant.const import CONF_DEVICE_CLASS, CONF_ICON, CONF_NAME, CONF_UNIQUE_ID
from homeassistant.core import HomeAssistant, callback
from homeassistant.const import (
ATTR_ENTITY_PICTURE,
ATTR_FRIENDLY_NAME,
ATTR_ICON,
CONF_DEVICE_CLASS,
CONF_ICON,
CONF_NAME,
CONF_UNIQUE_ID,
)
from homeassistant.core import HomeAssistant, State, callback
from homeassistant.exceptions import TemplateError
from homeassistant.helpers import template
from homeassistant.helpers.update_coordinator import CoordinatorEntity
@@ -13,6 +21,12 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import TriggerUpdateCoordinator
from .const import CONF_ATTRIBUTES, CONF_AVAILABILITY, CONF_PICTURE
CONF_TO_ATTRIBUTE = {
CONF_ICON: ATTR_ICON,
CONF_NAME: ATTR_FRIENDLY_NAME,
CONF_PICTURE: ATTR_ENTITY_PICTURE,
}
class TriggerEntity(CoordinatorEntity[TriggerUpdateCoordinator]):
"""Template entity based on trigger data."""
@@ -45,10 +59,10 @@ class TriggerEntity(CoordinatorEntity[TriggerUpdateCoordinator]):
self._to_render_complex: list[str] = []
for itm in (
CONF_NAME,
CONF_ICON,
CONF_PICTURE,
CONF_AVAILABILITY,
CONF_ICON,
CONF_NAME,
CONF_PICTURE,
):
if itm not in config:
continue
@@ -115,6 +129,21 @@ class TriggerEntity(CoordinatorEntity[TriggerUpdateCoordinator]):
if self.coordinator.data is not None:
self._process_data()
def restore_attributes(self, last_state: State) -> None:
"""Restore attributes."""
for conf_key, attr in CONF_TO_ATTRIBUTE.items():
if conf_key not in self._config or attr not in last_state.attributes:
continue
self._rendered[conf_key] = last_state.attributes[attr]
if CONF_ATTRIBUTES in self._config:
extra_state_attributes = {}
for attr in self._config[CONF_ATTRIBUTES]:
if attr not in last_state.attributes:
continue
extra_state_attributes[attr] = last_state.attributes[attr]
self._rendered[CONF_ATTRIBUTES] = extra_state_attributes
@callback
def _process_data(self) -> None:
"""Process new data."""

View File

@@ -103,7 +103,7 @@ RANDOM_EFFECT_DICT: Final = {
vol.Optional("random_seed", default=100): vol.All(
vol.Coerce(int), vol.Range(min=1, max=100)
),
vol.Required("backgrounds"): vol.All(
vol.Optional("backgrounds"): vol.All(
cv.ensure_list,
vol.Length(min=1, max=16),
[vol.All(vol.Coerce(tuple), HSV_SEQUENCE)],
@@ -366,7 +366,7 @@ class TPLinkSmartLightStrip(TPLinkSmartBulb):
fadeoff: int,
init_states: tuple[int, int, int],
random_seed: int,
backgrounds: Sequence[tuple[int, int, int]],
backgrounds: Sequence[tuple[int, int, int]] | None = None,
hue_range: tuple[int, int] | None = None,
saturation_range: tuple[int, int] | None = None,
brightness_range: tuple[int, int] | None = None,
@@ -378,8 +378,9 @@ class TPLinkSmartLightStrip(TPLinkSmartBulb):
"type": "random",
"init_states": [init_states],
"random_seed": random_seed,
"backgrounds": backgrounds,
}
if backgrounds:
effect["backgrounds"] = backgrounds
if fadeoff:
effect["fadeoff"] = fadeoff
if hue_range:

View File

@@ -93,7 +93,7 @@ random_effect:
- [199, 89, 50]
- [160, 50, 50]
- [180, 100, 50]
required: true
required: false
selector:
object:
segments:

View File

@@ -116,9 +116,6 @@ class ProtectFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
self.hass.config_entries.async_update_entry(
entry, data={**entry.data, CONF_HOST: new_host}
)
self.hass.async_create_task(
self.hass.config_entries.async_reload(entry.entry_id)
)
return self.async_abort(reason="already_configured")
if entry_host in (direct_connect_domain, source_ip) or (
entry_has_direct_connect

View File

@@ -106,27 +106,24 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
async def async_reset_meters(service_call):
"""Reset all sensors of a meter."""
entity_id = service_call.data["entity_id"]
meters = service_call.data["entity_id"]
domain = split_entity_id(entity_id)[0]
if domain == DOMAIN:
for entity in hass.data[DATA_LEGACY_COMPONENT].entities:
if entity_id == entity.entity_id:
_LOGGER.debug(
"forward reset meter from %s to %s",
entity_id,
entity.tracked_entity_id,
)
entity_id = entity.tracked_entity_id
_LOGGER.debug("reset meter %s", entity_id)
async_dispatcher_send(hass, SIGNAL_RESET_METER, entity_id)
for meter in meters:
_LOGGER.debug("resetting meter %s", meter)
domain, entity = split_entity_id(meter)
# backward compatibility up to 2022.07:
if domain == DOMAIN:
async_dispatcher_send(
hass, SIGNAL_RESET_METER, f"{SELECT_DOMAIN}.{entity}"
)
else:
async_dispatcher_send(hass, SIGNAL_RESET_METER, meter)
hass.services.async_register(
DOMAIN,
SERVICE_RESET,
async_reset_meters,
vol.Schema({ATTR_ENTITY_ID: cv.entity_id}),
vol.Schema({ATTR_ENTITY_ID: vol.All(cv.ensure_list, [cv.entity_id])}),
)
if DOMAIN not in config:

View File

@@ -6,7 +6,6 @@ reset:
target:
entity:
domain: select
integration: utility_meter
next_tariff:
name: Next Tariff

View File

@@ -2,7 +2,7 @@
"domain": "version",
"name": "Version",
"documentation": "https://www.home-assistant.io/integrations/version",
"requirements": ["pyhaversion==22.04.0"],
"requirements": ["pyhaversion==22.4.1"],
"codeowners": ["@fabaff", "@ludeeus"],
"quality_scale": "internal",
"iot_class": "local_push",

View File

@@ -29,6 +29,8 @@ from .const import (
CONF_SERIAL_NUMBER_KEY,
CONF_SOFTWARE_KEY,
CONF_STATION,
CONF_STATUS_DESCRIPTION_KEY,
CONF_STATUS_ID_KEY,
DOMAIN,
)
@@ -37,6 +39,39 @@ _LOGGER = logging.getLogger(__name__)
PLATFORMS = [Platform.SENSOR, Platform.NUMBER, Platform.LOCK]
UPDATE_INTERVAL = 30
# Translation of StatusId based on Wallbox portal code:
# https://my.wallbox.com/src/utilities/charger/chargerStatuses.js
CHARGER_STATUS: dict[int, str] = {
0: "Disconnected",
14: "Error",
15: "Error",
161: "Ready",
162: "Ready",
163: "Disconnected",
164: "Waiting",
165: "Locked",
166: "Updating",
177: "Scheduled",
178: "Paused",
179: "Scheduled",
180: "Waiting for car demand",
181: "Waiting for car demand",
182: "Paused",
183: "Waiting in queue by Power Sharing",
184: "Waiting in queue by Power Sharing",
185: "Waiting in queue by Power Boost",
186: "Waiting in queue by Power Boost",
187: "Waiting MID failed",
188: "Waiting MID safety margin exceeded",
189: "Waiting in queue by Eco-Smart",
193: "Charging",
194: "Charging",
195: "Charging",
196: "Discharging",
209: "Locked",
210: "Locked",
}
class WallboxCoordinator(DataUpdateCoordinator[dict[str, Any]]):
"""Wallbox Coordinator class."""
@@ -86,6 +121,9 @@ class WallboxCoordinator(DataUpdateCoordinator[dict[str, Any]]):
data[CONF_LOCKED_UNLOCKED_KEY] = data[CONF_DATA_KEY][
CONF_LOCKED_UNLOCKED_KEY
]
data[CONF_STATUS_DESCRIPTION_KEY] = CHARGER_STATUS.get(
data[CONF_STATUS_ID_KEY], "Unknown"
)
return data

View File

@@ -21,5 +21,6 @@ CONF_MAX_CHARGING_CURRENT_KEY = "max_charging_current"
CONF_LOCKED_UNLOCKED_KEY = "locked"
CONF_NAME_KEY = "name"
CONF_STATE_OF_CHARGE_KEY = "state_of_charge"
CONF_STATUS_ID_KEY = "status_id"
CONF_STATUS_DESCRIPTION_KEY = "status_description"
CONF_CONNECTIONS = "connections"

View File

@@ -232,7 +232,7 @@ GROUP_MEMBER_SCHEMA = vol.All(
vol.Schema(
{
vol.Required(ATTR_IEEE): IEEE_SCHEMA,
vol.Required(ATTR_ENDPOINT_ID): int,
vol.Required(ATTR_ENDPOINT_ID): vol.Coerce(int),
}
),
_cv_group_member,
@@ -244,8 +244,8 @@ CLUSTER_BINDING_SCHEMA = vol.All(
{
vol.Required(ATTR_NAME): cv.string,
vol.Required(ATTR_TYPE): cv.string,
vol.Required(ATTR_ID): int,
vol.Required(ATTR_ENDPOINT_ID): int,
vol.Required(ATTR_ID): vol.Coerce(int),
vol.Required(ATTR_ENDPOINT_ID): vol.Coerce(int),
}
),
_cv_cluster_binding,

View File

@@ -8,7 +8,12 @@ import logging
from typing import Any
import zigpy.exceptions
from zigpy.zcl.foundation import ConfigureReportingResponseRecord, Status
from zigpy.zcl.foundation import (
CommandSchema,
ConfigureReportingResponseRecord,
Status,
ZCLAttributeDef,
)
from homeassistant.const import ATTR_COMMAND
from homeassistant.core import callback
@@ -20,6 +25,7 @@ from ..const import (
ATTR_ATTRIBUTE_ID,
ATTR_ATTRIBUTE_NAME,
ATTR_CLUSTER_ID,
ATTR_PARAMS,
ATTR_TYPE,
ATTR_UNIQUE_ID,
ATTR_VALUE,
@@ -111,7 +117,11 @@ class ZigbeeChannel(LogMixin):
if not hasattr(self, "_value_attribute") and self.REPORT_CONFIG:
attr = self.REPORT_CONFIG[0].get("attr")
if isinstance(attr, str):
self.value_attribute = self.cluster.attributes_by_name.get(attr)
attribute: ZCLAttributeDef = self.cluster.attributes_by_name.get(attr)
if attribute is not None:
self.value_attribute = attribute.id
else:
self.value_attribute = None
else:
self.value_attribute = attr
self._status = ChannelStatus.CREATED
@@ -354,14 +364,27 @@ class ZigbeeChannel(LogMixin):
"""Handle ZDO commands on this cluster."""
@callback
def zha_send_event(self, command: str, args: int | dict) -> None:
def zha_send_event(self, command: str, arg: list | dict | CommandSchema) -> None:
"""Relay events to hass."""
if isinstance(arg, CommandSchema):
args = [a for a in arg if a is not None]
params = arg.as_dict()
elif isinstance(arg, (list, dict)):
# Quirks can directly send lists and dicts to ZHA this way
args = arg
params = {}
else:
raise TypeError(f"Unexpected zha_send_event {command!r} argument: {arg!r}")
self._ch_pool.zha_send_event(
{
ATTR_UNIQUE_ID: self.unique_id,
ATTR_CLUSTER_ID: self.cluster.cluster_id,
ATTR_COMMAND: command,
# Maintain backwards compatibility with the old zigpy response format
ATTR_ARGS: args,
ATTR_PARAMS: params,
}
)

View File

@@ -43,6 +43,7 @@ ATTR_NEIGHBORS = "neighbors"
ATTR_NODE_DESCRIPTOR = "node_descriptor"
ATTR_NWK = "nwk"
ATTR_OUT_CLUSTERS = "out_clusters"
ATTR_PARAMS = "params"
ATTR_POWER_SOURCE = "power_source"
ATTR_PROFILE_ID = "profile_id"
ATTR_QUIRK_APPLIED = "quirk_applied"

View File

@@ -661,7 +661,11 @@ class ZHADevice(LogMixin):
async def async_add_to_group(self, group_id: int) -> None:
"""Add this device to the provided zigbee group."""
try:
await self._zigpy_device.add_to_group(group_id)
# A group name is required. However, the spec also explicitly states that
# the group name can be ignored by the receiving device if a device cannot
# store it, so we cannot rely on it existing after being written. This is
# only done to make the ZCL command valid.
await self._zigpy_device.add_to_group(group_id, name=f"0x{group_id:04X}")
except (zigpy.exceptions.ZigbeeException, asyncio.TimeoutError) as ex:
self.debug(
"Failed to add device '%s' to group: 0x%04x ex: %s",
@@ -687,7 +691,9 @@ class ZHADevice(LogMixin):
) -> None:
"""Add the device endpoint to the provided zigbee group."""
try:
await self._zigpy_device.endpoints[endpoint_id].add_to_group(group_id)
await self._zigpy_device.endpoints[endpoint_id].add_to_group(
group_id, name=f"0x{group_id:04X}"
)
except (zigpy.exceptions.ZigbeeException, asyncio.TimeoutError) as ex:
self.debug(
"Failed to add endpoint: %s for device: '%s' to group: 0x%04x ex: %s",

View File

@@ -2,7 +2,6 @@
from __future__ import annotations
import asyncio
import collections
import logging
from typing import TYPE_CHECKING, Any, NamedTuple
@@ -30,9 +29,12 @@ class GroupMember(NamedTuple):
endpoint_id: int
GroupEntityReference = collections.namedtuple(
"GroupEntityReference", "name original_name entity_id"
)
class GroupEntityReference(NamedTuple):
"""Reference to a group entity."""
name: str
original_name: str
entity_id: int
class ZHAGroupMember(LogMixin):

View File

@@ -7,7 +7,7 @@
"bellows==0.29.0",
"pyserial==3.5",
"pyserial-asyncio==0.6",
"zha-quirks==0.0.69",
"zha-quirks==0.0.71",
"zigpy-deconz==0.15.0",
"zigpy==0.44.1",
"zigpy-xbee==0.14.0",

View File

@@ -113,6 +113,11 @@ DATA_INVALID_SERVER_VERSION_LOGGED = "invalid_server_version_logged"
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the Z-Wave JS component."""
hass.data[DOMAIN] = {}
for entry in hass.config_entries.async_entries(DOMAIN):
if not isinstance(entry.unique_id, str):
hass.config_entries.async_update_entry(
entry, unique_id=str(entry.unique_id)
)
return True
@@ -770,14 +775,3 @@ def async_ensure_addon_updated(hass: HomeAssistant) -> None:
if addon_manager.task_in_progress():
raise ConfigEntryNotReady
addon_manager.async_schedule_update_addon(catch_error=True)
async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Migrate old entry."""
if isinstance(config_entry.unique_id, int): # type: ignore[unreachable]
hass.config_entries.async_update_entry( # type: ignore[unreachable]
config_entry,
unique_id=str(config_entry.unique_id),
)
return True

View File

@@ -496,6 +496,7 @@ async def websocket_node_metadata(
"wakeup": node.device_config.metadata.wakeup,
"reset": node.device_config.metadata.reset,
"device_database_url": node.device_database_url,
"comments": node.device_config.metadata.comments,
}
connection.send_result(
msg[ID],

View File

@@ -667,7 +667,7 @@ class OptionsFlowHandler(BaseZwaveJSFlow, config_entries.OptionsFlow):
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
if self.config_entry.unique_id != version_info.home_id:
if self.config_entry.unique_id != str(version_info.home_id):
return self.async_abort(reason="different_device")
# Make sure we disable any add-on handling
@@ -827,7 +827,7 @@ class OptionsFlowHandler(BaseZwaveJSFlow, config_entries.OptionsFlow):
except CannotConnect:
return await self.async_revert_addon_config(reason="cannot_connect")
if self.config_entry.unique_id != self.version_info.home_id:
if self.config_entry.unique_id != str(self.version_info.home_id):
return await self.async_revert_addon_config(reason="different_device")
self._async_update_entry(

View File

@@ -7,7 +7,7 @@ from .backports.enum import StrEnum
MAJOR_VERSION: Final = 2022
MINOR_VERSION: Final = 4
PATCH_VERSION: Final = "0b6"
PATCH_VERSION: Final = "1"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0)

View File

@@ -110,7 +110,7 @@ aio_geojson_nsw_rfs_incidents==0.4
aio_georss_gdacs==0.5
# homeassistant.components.airzone
aioairzone==0.2.3
aioairzone==0.3.3
# homeassistant.components.ambient_station
aioambient==2021.11.0
@@ -740,7 +740,7 @@ googlemaps==2.5.1
goslide-api==0.5.1
# homeassistant.components.remote_rpi_gpio
gpiozero==1.5.1
gpiozero==1.6.2
# homeassistant.components.gpsd
gps3==0.33.3
@@ -1198,6 +1198,9 @@ phone_modem==0.1.1
# homeassistant.components.onewire
pi1wire==0.1.0
# homeassistant.components.remote_rpi_gpio
pigpio==1.78
# homeassistant.components.pilight
pilight==0.1.1
@@ -1511,7 +1514,7 @@ pygtfs==0.1.6
pygti==0.9.2
# homeassistant.components.version
pyhaversion==22.04.0
pyhaversion==22.4.1
# homeassistant.components.heos
pyheos==0.7.2
@@ -1799,7 +1802,7 @@ pysignalclirestapi==0.3.18
pyskyqhub==0.1.4
# homeassistant.components.sma
pysma==0.6.10
pysma==0.6.11
# homeassistant.components.smappee
pysmappee==0.2.29
@@ -2470,7 +2473,7 @@ zengge==0.2
zeroconf==0.38.4
# homeassistant.components.zha
zha-quirks==0.0.69
zha-quirks==0.0.71
# homeassistant.components.zhong_hong
zhong_hong_hvac==1.0.9

View File

@@ -94,7 +94,7 @@ aio_geojson_nsw_rfs_incidents==0.4
aio_georss_gdacs==0.5
# homeassistant.components.airzone
aioairzone==0.2.3
aioairzone==0.3.3
# homeassistant.components.ambient_station
aioambient==2021.11.0
@@ -999,7 +999,7 @@ pygatt[GATTTOOL]==4.0.5
pygti==0.9.2
# homeassistant.components.version
pyhaversion==22.04.0
pyhaversion==22.4.1
# homeassistant.components.heos
pyheos==0.7.2
@@ -1200,7 +1200,7 @@ pysiaalarm==3.0.2
pysignalclirestapi==0.3.18
# homeassistant.components.sma
pysma==0.6.10
pysma==0.6.11
# homeassistant.components.smappee
pysmappee==0.2.29
@@ -1601,7 +1601,7 @@ youless-api==0.16
zeroconf==0.38.4
# homeassistant.components.zha
zha-quirks==0.0.69
zha-quirks==0.0.71
# homeassistant.components.zha
zigpy-deconz==0.15.0

View File

@@ -1,6 +1,6 @@
[metadata]
name = homeassistant
version = 2022.4.0b6
version = 2022.4.1
author = The Home Assistant Authors
author_email = hello@home-assistant.io
license = Apache-2.0

View File

@@ -2,7 +2,7 @@
from unittest.mock import MagicMock, patch
from aiohttp.client_exceptions import ClientConnectorError
from aiohttp.client_exceptions import ClientConnectorError, ClientResponseError
from homeassistant import data_entry_flow
from homeassistant.components.airzone.const import DOMAIN
@@ -23,6 +23,12 @@ async def test_form(hass):
) as mock_setup_entry, patch(
"homeassistant.components.airzone.AirzoneLocalApi.get_hvac",
return_value=HVAC_MOCK,
), patch(
"homeassistant.components.airzone.AirzoneLocalApi.get_hvac_systems",
side_effect=ClientResponseError(MagicMock(), MagicMock()),
), patch(
"homeassistant.components.airzone.AirzoneLocalApi.get_webserver",
side_effect=ClientResponseError(MagicMock(), MagicMock()),
):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}

View File

@@ -6,7 +6,7 @@ MOCK_CONFIG = {
DOMAIN: {
CONF_DEVICES: [
{
CONF_HOST: "fake_host",
CONF_HOST: "10.0.0.1",
CONF_PASSWORD: "fake_pass",
CONF_USERNAME: "fake_user",
}

View File

@@ -2,6 +2,7 @@
import dataclasses
from unittest import mock
from unittest.mock import Mock, patch
from urllib.parse import urlparse
from pyfritzhome import LoginError
import pytest
@@ -24,15 +25,35 @@ from .const import CONF_FAKE_NAME, MOCK_CONFIG
from tests.common import MockConfigEntry
MOCK_USER_DATA = MOCK_CONFIG[DOMAIN][CONF_DEVICES][0]
MOCK_SSDP_DATA = ssdp.SsdpServiceInfo(
ssdp_usn="mock_usn",
ssdp_st="mock_st",
ssdp_location="https://fake_host:12345/test",
upnp={
ATTR_UPNP_FRIENDLY_NAME: CONF_FAKE_NAME,
ATTR_UPNP_UDN: "uuid:only-a-test",
},
)
MOCK_SSDP_DATA = {
"ip4_valid": ssdp.SsdpServiceInfo(
ssdp_usn="mock_usn",
ssdp_st="mock_st",
ssdp_location="https://10.0.0.1:12345/test",
upnp={
ATTR_UPNP_FRIENDLY_NAME: CONF_FAKE_NAME,
ATTR_UPNP_UDN: "uuid:only-a-test",
},
),
"ip6_valid": ssdp.SsdpServiceInfo(
ssdp_usn="mock_usn",
ssdp_st="mock_st",
ssdp_location="https://[1234::1]:12345/test",
upnp={
ATTR_UPNP_FRIENDLY_NAME: CONF_FAKE_NAME,
ATTR_UPNP_UDN: "uuid:only-a-test",
},
),
"ip6_invalid": ssdp.SsdpServiceInfo(
ssdp_usn="mock_usn",
ssdp_st="mock_st",
ssdp_location="https://[fe80::1%1]:12345/test",
upnp={
ATTR_UPNP_FRIENDLY_NAME: CONF_FAKE_NAME,
ATTR_UPNP_UDN: "uuid:only-a-test",
},
),
}
@pytest.fixture(name="fritz")
@@ -56,8 +77,8 @@ async def test_user(hass: HomeAssistant, fritz: Mock):
result["flow_id"], user_input=MOCK_USER_DATA
)
assert result["type"] == RESULT_TYPE_CREATE_ENTRY
assert result["title"] == "fake_host"
assert result["data"][CONF_HOST] == "fake_host"
assert result["title"] == "10.0.0.1"
assert result["data"][CONF_HOST] == "10.0.0.1"
assert result["data"][CONF_PASSWORD] == "fake_pass"
assert result["data"][CONF_USERNAME] == "fake_user"
assert not result["result"].unique_id
@@ -183,12 +204,29 @@ async def test_reauth_not_successful(hass: HomeAssistant, fritz: Mock):
assert result["reason"] == "no_devices_found"
async def test_ssdp(hass: HomeAssistant, fritz: Mock):
@pytest.mark.parametrize(
"test_data,expected_result",
[
(MOCK_SSDP_DATA["ip4_valid"], RESULT_TYPE_FORM),
(MOCK_SSDP_DATA["ip6_valid"], RESULT_TYPE_FORM),
(MOCK_SSDP_DATA["ip6_invalid"], RESULT_TYPE_ABORT),
],
)
async def test_ssdp(
hass: HomeAssistant,
fritz: Mock,
test_data: ssdp.SsdpServiceInfo,
expected_result: str,
):
"""Test starting a flow from discovery."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA
DOMAIN, context={"source": SOURCE_SSDP}, data=test_data
)
assert result["type"] == RESULT_TYPE_FORM
assert result["type"] == expected_result
if expected_result == RESULT_TYPE_ABORT:
return
assert result["step_id"] == "confirm"
result = await hass.config_entries.flow.async_configure(
@@ -197,7 +235,7 @@ async def test_ssdp(hass: HomeAssistant, fritz: Mock):
)
assert result["type"] == RESULT_TYPE_CREATE_ENTRY
assert result["title"] == CONF_FAKE_NAME
assert result["data"][CONF_HOST] == "fake_host"
assert result["data"][CONF_HOST] == urlparse(test_data.ssdp_location).hostname
assert result["data"][CONF_PASSWORD] == "fake_pass"
assert result["data"][CONF_USERNAME] == "fake_user"
assert result["result"].unique_id == "only-a-test"
@@ -205,7 +243,7 @@ async def test_ssdp(hass: HomeAssistant, fritz: Mock):
async def test_ssdp_no_friendly_name(hass: HomeAssistant, fritz: Mock):
"""Test starting a flow from discovery without friendly name."""
MOCK_NO_NAME = dataclasses.replace(MOCK_SSDP_DATA)
MOCK_NO_NAME = dataclasses.replace(MOCK_SSDP_DATA["ip4_valid"])
MOCK_NO_NAME.upnp = MOCK_NO_NAME.upnp.copy()
del MOCK_NO_NAME.upnp[ATTR_UPNP_FRIENDLY_NAME]
result = await hass.config_entries.flow.async_init(
@@ -219,8 +257,8 @@ async def test_ssdp_no_friendly_name(hass: HomeAssistant, fritz: Mock):
user_input={CONF_PASSWORD: "fake_pass", CONF_USERNAME: "fake_user"},
)
assert result["type"] == RESULT_TYPE_CREATE_ENTRY
assert result["title"] == "fake_host"
assert result["data"][CONF_HOST] == "fake_host"
assert result["title"] == "10.0.0.1"
assert result["data"][CONF_HOST] == "10.0.0.1"
assert result["data"][CONF_PASSWORD] == "fake_pass"
assert result["data"][CONF_USERNAME] == "fake_user"
assert result["result"].unique_id == "only-a-test"
@@ -231,7 +269,7 @@ async def test_ssdp_auth_failed(hass: HomeAssistant, fritz: Mock):
fritz().login.side_effect = LoginError("Boom")
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA
DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA["ip4_valid"]
)
assert result["type"] == RESULT_TYPE_FORM
assert result["step_id"] == "confirm"
@@ -251,7 +289,7 @@ async def test_ssdp_not_successful(hass: HomeAssistant, fritz: Mock):
fritz().login.side_effect = OSError("Boom")
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA
DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA["ip4_valid"]
)
assert result["type"] == RESULT_TYPE_FORM
assert result["step_id"] == "confirm"
@@ -269,7 +307,7 @@ async def test_ssdp_not_supported(hass: HomeAssistant, fritz: Mock):
fritz().get_device_elements.side_effect = HTTPError("Boom")
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA
DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA["ip4_valid"]
)
assert result["type"] == RESULT_TYPE_FORM
assert result["step_id"] == "confirm"
@@ -285,13 +323,13 @@ async def test_ssdp_not_supported(hass: HomeAssistant, fritz: Mock):
async def test_ssdp_already_in_progress_unique_id(hass: HomeAssistant, fritz: Mock):
"""Test starting a flow from discovery twice."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA
DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA["ip4_valid"]
)
assert result["type"] == RESULT_TYPE_FORM
assert result["step_id"] == "confirm"
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA
DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA["ip4_valid"]
)
assert result["type"] == RESULT_TYPE_ABORT
assert result["reason"] == "already_in_progress"
@@ -300,12 +338,12 @@ async def test_ssdp_already_in_progress_unique_id(hass: HomeAssistant, fritz: Mo
async def test_ssdp_already_in_progress_host(hass: HomeAssistant, fritz: Mock):
"""Test starting a flow from discovery twice."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA
DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA["ip4_valid"]
)
assert result["type"] == RESULT_TYPE_FORM
assert result["step_id"] == "confirm"
MOCK_NO_UNIQUE_ID = dataclasses.replace(MOCK_SSDP_DATA)
MOCK_NO_UNIQUE_ID = dataclasses.replace(MOCK_SSDP_DATA["ip4_valid"])
MOCK_NO_UNIQUE_ID.upnp = MOCK_NO_UNIQUE_ID.upnp.copy()
del MOCK_NO_UNIQUE_ID.upnp[ATTR_UPNP_UDN]
result = await hass.config_entries.flow.async_init(
@@ -324,7 +362,7 @@ async def test_ssdp_already_configured(hass: HomeAssistant, fritz: Mock):
assert not result["result"].unique_id
result2 = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA
DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA["ip4_valid"]
)
assert result2["type"] == RESULT_TYPE_ABORT
assert result2["reason"] == "already_configured"

View File

@@ -35,12 +35,12 @@ async def test_setup(hass: HomeAssistant, fritz: Mock):
entries = hass.config_entries.async_entries()
assert entries
assert len(entries) == 1
assert entries[0].data[CONF_HOST] == "fake_host"
assert entries[0].data[CONF_HOST] == "10.0.0.1"
assert entries[0].data[CONF_PASSWORD] == "fake_pass"
assert entries[0].data[CONF_USERNAME] == "fake_user"
assert fritz.call_count == 1
assert fritz.call_args_list == [
call(host="fake_host", password="fake_pass", user="fake_user")
call(host="10.0.0.1", password="fake_pass", user="fake_user")
]

View File

@@ -10,6 +10,7 @@ import pytest
import respx
from homeassistant import config_entries, data_entry_flow, setup
from homeassistant.components.camera import async_get_image
from homeassistant.components.generic.const import (
CONF_CONTENT_TYPE,
CONF_FRAMERATE,
@@ -191,7 +192,7 @@ async def test_form_rtsp_mode(hass, fakeimg_png, mock_av_open, user_flow):
assert len(mock_setup.mock_calls) == 1
async def test_form_only_stream(hass, mock_av_open):
async def test_form_only_stream(hass, mock_av_open, fakeimgbytes_jpg):
"""Test we complete ok if the user wants stream only."""
await setup.async_setup_component(hass, "persistent_notification", {})
result = await hass.config_entries.flow.async_init(
@@ -204,21 +205,34 @@ async def test_form_only_stream(hass, mock_av_open):
result["flow_id"],
data,
)
assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result2["title"] == "127_0_0_1_testurl_2"
assert result2["options"] == {
assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
result3 = await hass.config_entries.flow.async_configure(
result2["flow_id"],
{CONF_CONTENT_TYPE: "image/jpeg"},
)
assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result3["title"] == "127_0_0_1_testurl_2"
assert result3["options"] == {
CONF_AUTHENTICATION: HTTP_BASIC_AUTHENTICATION,
CONF_STREAM_SOURCE: "http://127.0.0.1/testurl/2",
CONF_RTSP_TRANSPORT: "tcp",
CONF_USERNAME: "fred_flintstone",
CONF_PASSWORD: "bambam",
CONF_LIMIT_REFETCH_TO_URL_CHANGE: False,
CONF_CONTENT_TYPE: None,
CONF_CONTENT_TYPE: "image/jpeg",
CONF_FRAMERATE: 5,
CONF_VERIFY_SSL: False,
}
await hass.async_block_till_done()
with patch(
"homeassistant.components.generic.camera.GenericCamera.async_camera_image",
return_value=fakeimgbytes_jpg,
):
image_obj = await async_get_image(hass, "camera.127_0_0_1_testurl_2")
assert image_obj.content == fakeimgbytes_jpg
assert len(mock_setup.mock_calls) == 1
@@ -478,6 +492,45 @@ async def test_options_template_error(hass, fakeimgbytes_png, mock_av_open):
assert result4["errors"] == {"still_image_url": "template_error"}
@respx.mock
async def test_options_only_stream(hass, fakeimgbytes_png, mock_av_open):
"""Test the options flow without a still_image_url."""
respx.get("http://127.0.0.1/testurl/2").respond(stream=fakeimgbytes_png)
data = TESTDATA.copy()
data.pop(CONF_STILL_IMAGE_URL)
mock_entry = MockConfigEntry(
title="Test Camera",
domain=DOMAIN,
data={},
options=data,
)
with mock_av_open:
mock_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_entry.entry_id)
await hass.async_block_till_done()
result = await hass.config_entries.options.async_init(mock_entry.entry_id)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "init"
# try updating the config options
result2 = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input=data,
)
# Should be shown a 2nd form
assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result2["step_id"] == "content_type"
result3 = await hass.config_entries.options.async_configure(
result2["flow_id"],
user_input={CONF_CONTENT_TYPE: "image/png"},
)
assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result3["data"][CONF_CONTENT_TYPE] == "image/png"
# These below can be deleted after deprecation period is finished.
@respx.mock
async def test_import(hass, fakeimg_png, mock_av_open):

View File

@@ -272,6 +272,35 @@ async def test_all_day_offset_event(hass, mock_events_list_items, component_setu
}
async def test_missing_summary(hass, mock_events_list_items, component_setup):
"""Test that we can create an event trigger on device."""
start_event = dt_util.now() + datetime.timedelta(minutes=14)
end_event = start_event + datetime.timedelta(minutes=60)
event = {
**TEST_EVENT,
"start": {"dateTime": start_event.isoformat()},
"end": {"dateTime": end_event.isoformat()},
}
del event["summary"]
mock_events_list_items([event])
assert await component_setup()
state = hass.states.get(TEST_ENTITY)
assert state.name == TEST_ENTITY_NAME
assert state.state == STATE_OFF
assert dict(state.attributes) == {
"friendly_name": TEST_ENTITY_NAME,
"message": "",
"all_day": False,
"offset_reached": False,
"start_time": start_event.strftime(DATE_STR_FORMAT),
"end_time": end_event.strftime(DATE_STR_FORMAT),
"location": event["location"],
"description": event["description"],
}
async def test_update_error(
hass, calendar_resource, component_setup, test_api_calendar
):

View File

@@ -97,6 +97,12 @@ async def test_full_flow(
assert "data" in result
data = result["data"]
assert "token" in data
assert 0 < data["token"]["expires_in"] < 8 * 86400
assert (
datetime.datetime.now().timestamp()
<= data["token"]["expires_at"]
< (datetime.datetime.now() + datetime.timedelta(days=8)).timestamp()
)
data["token"].pop("expires_at")
data["token"].pop("expires_in")
assert data == {

View File

@@ -24,7 +24,11 @@ def mock_all(aioclient_mock, request):
"http://127.0.0.1/info",
json={
"result": "ok",
"data": {"supervisor": "222", "homeassistant": "0.110.0", "hassos": None},
"data": {
"supervisor": "222",
"homeassistant": "0.110.0",
"hassos": "1.2.3",
},
},
)
aioclient_mock.get(

View File

@@ -30,7 +30,11 @@ def mock_all(aioclient_mock, request):
"http://127.0.0.1/info",
json={
"result": "ok",
"data": {"supervisor": "222", "homeassistant": "0.110.0", "hassos": None},
"data": {
"supervisor": "222",
"homeassistant": "0.110.0",
"hassos": "1.2.3",
},
},
)
aioclient_mock.get(
@@ -396,14 +400,14 @@ async def test_service_calls(hassio_env, hass, aioclient_mock, caplog):
)
await hass.async_block_till_done()
assert aioclient_mock.call_count == 8
assert aioclient_mock.call_count == 9
assert aioclient_mock.mock_calls[-1][2] == "test"
await hass.services.async_call("hassio", "host_shutdown", {})
await hass.services.async_call("hassio", "host_reboot", {})
await hass.async_block_till_done()
assert aioclient_mock.call_count == 10
assert aioclient_mock.call_count == 11
await hass.services.async_call("hassio", "backup_full", {})
await hass.services.async_call(
@@ -418,7 +422,7 @@ async def test_service_calls(hassio_env, hass, aioclient_mock, caplog):
)
await hass.async_block_till_done()
assert aioclient_mock.call_count == 12
assert aioclient_mock.call_count == 13
assert aioclient_mock.mock_calls[-1][2] == {
"homeassistant": True,
"addons": ["test"],
@@ -442,7 +446,7 @@ async def test_service_calls(hassio_env, hass, aioclient_mock, caplog):
)
await hass.async_block_till_done()
assert aioclient_mock.call_count == 14
assert aioclient_mock.call_count == 15
assert aioclient_mock.mock_calls[-1][2] == {
"addons": ["test"],
"folders": ["ssl"],
@@ -461,12 +465,12 @@ async def test_service_calls_core(hassio_env, hass, aioclient_mock):
await hass.services.async_call("homeassistant", "stop")
await hass.async_block_till_done()
assert aioclient_mock.call_count == 4
assert aioclient_mock.call_count == 5
await hass.services.async_call("homeassistant", "check_config")
await hass.async_block_till_done()
assert aioclient_mock.call_count == 4
assert aioclient_mock.call_count == 5
with patch(
"homeassistant.config.async_check_ha_config_file", return_value=None
@@ -475,7 +479,7 @@ async def test_service_calls_core(hassio_env, hass, aioclient_mock):
await hass.async_block_till_done()
assert mock_check_config.called
assert aioclient_mock.call_count == 5
assert aioclient_mock.call_count == 6
async def test_entry_load_and_unload(hass):
@@ -628,10 +632,17 @@ async def test_device_registry_calls(hass):
), patch(
"homeassistant.components.hassio.HassIO.get_os_info",
return_value=os_mock_data,
), patch(
"homeassistant.components.hassio.HassIO.get_info",
return_value={
"supervisor": "222",
"homeassistant": "0.110.0",
"hassos": None,
},
):
async_fire_time_changed(hass, dt_util.now() + timedelta(hours=3))
await hass.async_block_till_done()
assert len(dev_reg.devices) == 5
assert len(dev_reg.devices) == 4
async def test_coordinator_updates(hass, caplog):

View File

@@ -24,7 +24,11 @@ def mock_all(aioclient_mock, request):
"http://127.0.0.1/info",
json={
"result": "ok",
"data": {"supervisor": "222", "homeassistant": "0.110.0", "hassos": None},
"data": {
"supervisor": "222",
"homeassistant": "0.110.0",
"hassos": "1.2.3",
},
},
)
aioclient_mock.get(

View File

@@ -25,7 +25,11 @@ def mock_all(aioclient_mock, request):
"http://127.0.0.1/info",
json={
"result": "ok",
"data": {"supervisor": "222", "homeassistant": "0.110.0", "hassos": None},
"data": {
"supervisor": "222",
"homeassistant": "0.110.0",
"hassos": "1.2.3",
},
},
)
aioclient_mock.get(
@@ -483,3 +487,25 @@ async def test_not_release_notes(hass, aioclient_mock, hass_ws_client):
)
result = await client.receive_json()
assert result["result"] is None
async def test_no_os_entity(hass):
"""Test handling where there is no os entity."""
with patch.dict(os.environ, MOCK_ENVIRON), patch(
"homeassistant.components.hassio.HassIO.get_info",
return_value={
"supervisor": "222",
"homeassistant": "0.110.0",
"hassos": None,
},
):
result = await async_setup_component(
hass,
"hassio",
{"http": {"server_port": 9999, "server_host": "127.0.0.1"}, "hassio": {}},
)
assert result
await hass.async_block_till_done()
# Verify that the entity does not exist
assert not hass.states.get("update.home_assistant_operating_system_update")

View File

@@ -1347,6 +1347,16 @@ async def test_options_flow_exclude_mode_skips_category_entities(
entity_category=EntityCategory.CONFIG,
)
hass.states.async_set(sonos_config_switch.entity_id, "off")
sonos_notconfig_switch: RegistryEntry = entity_reg.async_get_or_create(
"switch",
"sonos",
"notconfig",
device_id="1234",
entity_category=None,
)
hass.states.async_set(sonos_notconfig_switch.entity_id, "off")
await hass.async_block_till_done()
result = await hass.config_entries.options.async_init(
@@ -1391,14 +1401,24 @@ async def test_options_flow_exclude_mode_skips_category_entities(
result4 = await hass.config_entries.options.async_configure(
result2["flow_id"],
user_input={"entities": ["media_player.tv", "switch.other"]},
user_input={
"entities": [
"media_player.tv",
"switch.other",
sonos_notconfig_switch.entity_id,
]
},
)
assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert config_entry.options == {
"mode": "bridge",
"filter": {
"exclude_domains": [],
"exclude_entities": ["media_player.tv", "switch.other"],
"exclude_entities": [
"media_player.tv",
"switch.other",
sonos_notconfig_switch.entity_id,
],
"include_domains": ["media_player", "switch"],
"include_entities": [],
},

View File

@@ -1,5 +1,7 @@
"""The sensor tests for the powerwall platform."""
from unittest.mock import patch
from unittest.mock import Mock, patch
from tesla_powerwall.error import MissingAttributeError
from homeassistant.components.powerwall.const import DOMAIN
from homeassistant.components.sensor import ATTR_STATE_CLASS
@@ -112,3 +114,26 @@ async def test_sensors(hass, entity_registry_enabled_by_default):
# HA changes the implementation and a new one appears
for key, value in expected_attributes.items():
assert state.attributes[key] == value
async def test_sensor_backup_reserve_unavailable(hass):
"""Confirm that backup reserve sensor is not added if data is unavailable from the device."""
mock_powerwall = await _mock_powerwall_with_fixtures(hass)
mock_powerwall.get_backup_reserve_percentage = Mock(
side_effect=MissingAttributeError(Mock(), "backup_reserve_percent", "operation")
)
config_entry = MockConfigEntry(domain=DOMAIN, data={CONF_IP_ADDRESS: "1.2.3.4"})
config_entry.add_to_hass(hass)
with patch(
"homeassistant.components.powerwall.config_flow.Powerwall",
return_value=mock_powerwall,
), patch(
"homeassistant.components.powerwall.Powerwall", return_value=mock_powerwall
):
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
state = hass.states.get("sensor.powerwall_backup_reserve")
assert state is None

View File

@@ -2,7 +2,6 @@
from unittest.mock import patch
import pytest
from telegram.ext.dispatcher import Dispatcher
from homeassistant.components.telegram_bot import (
CONF_ALLOWED_CHAT_IDS,
@@ -176,12 +175,3 @@ async def polling_platform(hass, config_polling):
config_polling,
)
await hass.async_block_till_done()
@pytest.fixture(autouse=True)
def clear_dispatcher():
"""Clear the singleton that telegram.ext.dispatcher.Dispatcher sets on itself."""
yield
Dispatcher._set_singleton(None)
# This is how python-telegram-bot resets the dispatcher in their test suite
Dispatcher._Dispatcher__singleton_semaphore.release()

View File

@@ -0,0 +1,20 @@
"""Test Telegram broadcast."""
from homeassistant.setup import async_setup_component
async def test_setup(hass):
"""Test setting up Telegram broadcast."""
assert await async_setup_component(
hass,
"telegram_bot",
{
"telegram_bot": {
"platform": "broadcast",
"api_key": "1234567890:ABC",
"allowed_chat_ids": [1],
}
},
)
await hass.async_block_till_done()
assert hass.services.has_service("telegram_bot", "send_message") is True

View File

@@ -1,4 +1,5 @@
"""Tests for the telegram_bot component."""
import pytest
from telegram import Update
from telegram.ext.dispatcher import Dispatcher
@@ -8,6 +9,15 @@ from homeassistant.components.telegram_bot.webhooks import TELEGRAM_WEBHOOK_URL
from tests.common import async_capture_events
@pytest.fixture(autouse=True)
def clear_dispatcher():
"""Clear the singleton that telegram.ext.dispatcher.Dispatcher sets on itself."""
yield
Dispatcher._set_singleton(None)
# This is how python-telegram-bot resets the dispatcher in their test suite
Dispatcher._Dispatcher__singleton_semaphore.release()
async def test_webhook_platform_init(hass, webhook_platform):
"""Test initialization of the webhooks platform."""
assert hass.services.has_service(DOMAIN, SERVICE_SEND_MESSAGE) is True

View File

@@ -1142,29 +1142,41 @@ async def test_template_with_trigger_templated_delay_on(hass, start_ha):
"name": "test",
"state": "{{ trigger.event.data.beer == 2 }}",
"device_class": "motion",
"picture": "{{ '/local/dogs.png' }}",
"icon": "{{ 'mdi:pirate' }}",
"attributes": {
"plus_one": "{{ trigger.event.data.beer + 1 }}",
"another": "{{ trigger.event.data.uno_mas or 1 }}",
},
},
},
},
],
)
@pytest.mark.parametrize(
"restored_state, initial_state",
"restored_state, initial_state, initial_attributes",
[
(ON, ON),
(OFF, OFF),
(STATE_UNAVAILABLE, STATE_UNKNOWN),
(STATE_UNKNOWN, STATE_UNKNOWN),
(ON, ON, ["entity_picture", "icon", "plus_one"]),
(OFF, OFF, ["entity_picture", "icon", "plus_one"]),
(STATE_UNAVAILABLE, STATE_UNKNOWN, []),
(STATE_UNKNOWN, STATE_UNKNOWN, []),
],
)
async def test_trigger_entity_restore_state(
hass, count, domain, config, restored_state, initial_state
hass, count, domain, config, restored_state, initial_state, initial_attributes
):
"""Test restoring trigger template binary sensor."""
restored_attributes = {
"entity_picture": "/local/cats.png",
"icon": "mdi:ship",
"plus_one": 55,
}
fake_state = State(
"binary_sensor.test",
restored_state,
{},
restored_attributes,
)
fake_extra_data = {
"auto_off_time": None,
@@ -1183,6 +1195,22 @@ async def test_trigger_entity_restore_state(
state = hass.states.get("binary_sensor.test")
assert state.state == initial_state
for attr in restored_attributes:
if attr in initial_attributes:
assert state.attributes[attr] == restored_attributes[attr]
else:
assert attr not in state.attributes
assert "another" not in state.attributes
hass.bus.async_fire("test_event", {"beer": 2})
await hass.async_block_till_done()
state = hass.states.get("binary_sensor.test")
assert state.state == ON
assert state.attributes["icon"] == "mdi:pirate"
assert state.attributes["entity_picture"] == "/local/dogs.png"
assert state.attributes["plus_one"] == 3
assert state.attributes["another"] == 1
@pytest.mark.parametrize("count,domain", [(1, "template")])

View File

@@ -517,6 +517,33 @@ async def test_smart_strip_custom_random_effect(hass: HomeAssistant) -> None:
)
strip.set_custom_effect.reset_mock()
await hass.services.async_call(
DOMAIN,
"random_effect",
{
ATTR_ENTITY_ID: entity_id,
"init_states": [340, 20, 50],
},
blocking=True,
)
strip.set_custom_effect.assert_called_once_with(
{
"custom": 1,
"id": "yMwcNpLxijmoKamskHCvvravpbnIqAIN",
"brightness": 100,
"name": "Custom",
"segments": [0],
"expansion_strategy": 1,
"enable": 1,
"duration": 0,
"transition": 0,
"type": "random",
"init_states": [[340, 20, 50]],
"random_seed": 100,
}
)
strip.set_custom_effect.reset_mock()
strip.effect = {
"custom": 1,
"id": "yMwcNpLxijmoKamskHCvvravpbnIqAIN",

View File

@@ -365,10 +365,7 @@ async def test_discovered_by_unifi_discovery_direct_connect_updated(
)
mock_config.add_to_hass(hass)
with _patch_discovery(), patch(
"homeassistant.components.unifiprotect.async_setup_entry",
return_value=True,
) as mock_setup_entry:
with _patch_discovery():
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY},
@@ -378,7 +375,6 @@ async def test_discovered_by_unifi_discovery_direct_connect_updated(
assert result["type"] == RESULT_TYPE_ABORT
assert result["reason"] == "already_configured"
assert len(mock_setup_entry.mock_calls) == 1
assert mock_config.data[CONF_HOST] == DIRECT_CONNECT_DOMAIN
@@ -401,10 +397,7 @@ async def test_discovered_by_unifi_discovery_direct_connect_updated_but_not_usin
)
mock_config.add_to_hass(hass)
with _patch_discovery(), patch(
"homeassistant.components.unifiprotect.async_setup_entry",
return_value=True,
) as mock_setup_entry:
with _patch_discovery():
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY},
@@ -414,7 +407,6 @@ async def test_discovered_by_unifi_discovery_direct_connect_updated_but_not_usin
assert result["type"] == RESULT_TYPE_ABORT
assert result["reason"] == "already_configured"
assert len(mock_setup_entry.mock_calls) == 1
assert mock_config.data[CONF_HOST] == "127.0.0.1"

View File

@@ -65,7 +65,16 @@ async def test_restore_state(hass):
assert state.state == "midpeak"
async def test_services(hass):
@pytest.mark.parametrize(
"meter",
(
["select.energy_bill"],
"select.energy_bill",
["utility_meter.energy_bill"],
"utility_meter.energy_bill",
),
)
async def test_services(hass, meter):
"""Test energy sensor reset service."""
config = {
"utility_meter": {
@@ -159,7 +168,7 @@ async def test_services(hass):
assert state.state == "1"
# Reset meters
data = {ATTR_ENTITY_ID: "select.energy_bill"}
data = {ATTR_ENTITY_ID: meter}
await hass.services.async_call(DOMAIN, SERVICE_RESET, data)
await hass.async_block_till_done()

View File

@@ -20,6 +20,7 @@ from homeassistant.components.wallbox.const import (
CONF_SERIAL_NUMBER_KEY,
CONF_SOFTWARE_KEY,
CONF_STATION,
CONF_STATUS_ID_KEY,
DOMAIN,
)
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
@@ -33,6 +34,7 @@ test_response = json.loads(
json.dumps(
{
CONF_CHARGING_POWER_KEY: 0,
CONF_STATUS_ID_KEY: 161,
CONF_MAX_AVAILABLE_POWER_KEY: 25.2,
CONF_CHARGING_SPEED_KEY: 0,
CONF_ADDED_RANGE_KEY: 150,

View File

@@ -140,3 +140,13 @@ async def test_action(hass, device_ias):
assert calls[0].domain == DOMAIN
assert calls[0].service == "warning_device_warn"
assert calls[0].data["ieee"] == ieee_address
async def test_invalid_zha_event_type(hass, device_ias):
"""Test that unexpected types are not passed to `zha_send_event`."""
zigpy_device, zha_device = device_ias
channel = zha_device.channels.pools[0].client_channels["1:0x0006"]
# `zha_send_event` accepts only zigpy responses, lists, and dicts
with pytest.raises(TypeError):
channel.zha_send_event(COMMAND_SINGLE, 123)

View File

@@ -68,7 +68,11 @@
"inclusion": "To add the ZP3111 to the Z-Wave network (inclusion), place the Z-Wave primary controller into inclusion mode. Press the Program Switch of ZP3111 for sending the NIF. After sending NIF, Z-Wave will send the auto inclusion, otherwise, ZP3111 will go to sleep after 20 seconds.",
"exclusion": "To remove the ZP3111 from the Z-Wave network (exclusion), place the Z-Wave primary controller into \u201cexclusion\u201d mode, and following its instruction to delete the ZP3111 to the controller. Press the Program Switch of ZP3111 once to be excluded.",
"reset": "Remove cover to triggered tamper switch, LED flash once & send out Alarm Report. Press Program Switch 10 times within 10 seconds, ZP3111 will send the \u201cDevice Reset Locally Notification\u201d command and reset to the factory default. (Remark: This is to be used only in the case of primary controller being inoperable or otherwise unavailable.)",
"manual": "https://products.z-wavealliance.org/ProductManual/File?folder=&filename=MarketCertificationFiles/2479/ZP3111-5_R2_20170316.pdf"
"manual": "https://products.z-wavealliance.org/ProductManual/File?folder=&filename=MarketCertificationFiles/2479/ZP3111-5_R2_20170316.pdf",
"comments": {
"level": "info",
"text": "test"
}
},
"isEmbedded": true
},

View File

@@ -249,6 +249,7 @@ async def test_node_metadata(hass, wallmote_central_scene, integration, hass_ws_
result["device_database_url"]
== "https://devices.zwave-js.io/?jumpTo=0x0086:0x0002:0x0082:0.0"
)
assert result["comments"] == [{"level": "info", "text": "test"}]
# Test getting non-existent node fails
await ws_client.send_json(

View File

@@ -811,7 +811,10 @@ async def test_abort_usb_discovery_with_existing_flow(hass, supervisor, addon_op
async def test_abort_usb_discovery_already_configured(hass, supervisor, addon_options):
"""Test usb discovery flow is aborted when there is an existing entry."""
entry = MockConfigEntry(
domain=DOMAIN, data={"url": "ws://localhost:3000"}, title=TITLE, unique_id=1234
domain=DOMAIN,
data={"url": "ws://localhost:3000"},
title=TITLE,
unique_id="1234",
)
entry.add_to_hass(hass)
@@ -1042,9 +1045,10 @@ async def test_addon_running_already_configured(
"s2_unauthenticated_key": "old987",
},
title=TITLE,
unique_id="1234",
unique_id=1234, # Unique ID is purposely set to int to test migration logic
)
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
@@ -1560,7 +1564,7 @@ async def test_install_addon_failure(hass, supervisor, addon_installed, install_
async def test_options_manual(hass, client, integration):
"""Test manual settings in options flow."""
entry = integration
entry.unique_id = 1234
entry.unique_id = "1234"
assert client.connect.call_count == 1
assert client.disconnect.call_count == 0
@@ -1605,7 +1609,7 @@ async def test_options_manual_different_device(hass, integration):
async def test_options_not_addon(hass, client, supervisor, integration):
"""Test options flow and opting out of add-on on Supervisor."""
entry = integration
entry.unique_id = 1234
entry.unique_id = "1234"
assert client.connect.call_count == 1
assert client.disconnect.call_count == 0
@@ -1706,7 +1710,7 @@ async def test_options_addon_running(
"""Test options flow and add-on already running on Supervisor."""
addon_options.update(old_addon_options)
entry = integration
entry.unique_id = 1234
entry.unique_id = "1234"
data = {**entry.data, **entry_data}
hass.config_entries.async_update_entry(entry, data=data)
@@ -1816,7 +1820,7 @@ async def test_options_addon_running_no_changes(
"""Test options flow without changes, and add-on already running on Supervisor."""
addon_options.update(old_addon_options)
entry = integration
entry.unique_id = 1234
entry.unique_id = "1234"
data = {**entry.data, **entry_data}
hass.config_entries.async_update_entry(entry, data=data)
@@ -1929,7 +1933,7 @@ async def test_options_different_device(
"""Test options flow and configuring a different device."""
addon_options.update(old_addon_options)
entry = integration
entry.unique_id = 1234
entry.unique_id = "1234"
data = {**entry.data, **entry_data}
hass.config_entries.async_update_entry(entry, data=data)
@@ -2079,7 +2083,7 @@ async def test_options_addon_restart_failed(
"""Test options flow and add-on restart failure."""
addon_options.update(old_addon_options)
entry = integration
entry.unique_id = 1234
entry.unique_id = "1234"
data = {**entry.data, **entry_data}
hass.config_entries.async_update_entry(entry, data=data)
@@ -2200,7 +2204,7 @@ async def test_options_addon_running_server_info_failure(
"""Test options flow and add-on already running with server info failure."""
addon_options.update(old_addon_options)
entry = integration
entry.unique_id = 1234
entry.unique_id = "1234"
data = {**entry.data, **entry_data}
hass.config_entries.async_update_entry(entry, data=data)
@@ -2304,7 +2308,7 @@ async def test_options_addon_not_installed(
addon_installed.return_value["version"] = None
addon_options.update(old_addon_options)
entry = integration
entry.unique_id = 1234
entry.unique_id = "1234"
data = {**entry.data, **entry_data}
hass.config_entries.async_update_entry(entry, data=data)

View File

@@ -8,7 +8,6 @@ from zwave_js_server.exceptions import BaseZwaveJSServerError, InvalidServerVers
from zwave_js_server.model.node import Node
from homeassistant.components.hassio.handler import HassioAPIError
from homeassistant.components.zwave_js import async_migrate_entry
from homeassistant.components.zwave_js.const import DOMAIN
from homeassistant.components.zwave_js.helpers import get_device_id
from homeassistant.config_entries import ConfigEntryDisabler, ConfigEntryState
@@ -1328,12 +1327,3 @@ async def test_disabled_entity_on_value_removed(hass, zp3111, client, integratio
| {battery_level_entity, binary_cover_entity, sensor_cover_entity}
== new_unavailable_entities
)
async def test_async_migrate_entry(hass):
"""Test async_migrate_entry."""
entry = MockConfigEntry(domain=DOMAIN, unique_id=123456789)
assert isinstance(entry.unique_id, int)
await async_migrate_entry(hass, entry)
assert isinstance(entry.unique_id, str)
assert entry.unique_id == "123456789"

View File

@@ -8,8 +8,12 @@ import pytest
from homeassistant import config_entries, data_entry_flow, loader
from homeassistant.components.hassio import HassioServiceInfo
from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP
from homeassistant.core import CoreState, callback
from homeassistant.const import (
EVENT_COMPONENT_LOADED,
EVENT_HOMEASSISTANT_STARTED,
EVENT_HOMEASSISTANT_STOP,
)
from homeassistant.core import CoreState, Event, callback
from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, BaseServiceInfo
from homeassistant.exceptions import (
ConfigEntryAuthFailed,
@@ -2299,6 +2303,72 @@ async def test_async_setup_init_entry(hass):
assert entries[0].state is config_entries.ConfigEntryState.LOADED
async def test_async_setup_init_entry_completes_before_loaded_event_fires(hass):
"""Test a config entry being initialized during integration setup before the loaded event fires."""
@callback
def _record_load(event: Event) -> None:
nonlocal load_events
load_events.append(event)
listener = hass.bus.async_listen(EVENT_COMPONENT_LOADED, _record_load)
load_events: list[Event] = []
async def mock_async_setup(hass, config):
"""Mock setup."""
hass.async_create_task(
hass.config_entries.flow.async_init(
"comp",
context={"source": config_entries.SOURCE_IMPORT},
data={},
)
)
return True
async_setup_entry = AsyncMock(return_value=True)
mock_integration(
hass,
MockModule(
"comp", async_setup=mock_async_setup, async_setup_entry=async_setup_entry
),
)
mock_entity_platform(hass, "config_flow.comp", None)
class TestFlow(config_entries.ConfigFlow):
"""Test flow."""
VERSION = 1
async def async_step_three(self, user_input=None):
"""Test import step creating entry."""
return self.async_create_entry(title="title", data={})
async def async_step_two(self, user_input=None):
"""Test import step creating entry."""
return await self.async_step_three()
async def async_step_one(self, user_input=None):
"""Test import step creating entry."""
return await self.async_step_two()
async def async_step_import(self, user_input=None):
"""Test import step creating entry."""
return await self.async_step_one()
# This test must not use hass.async_block_till_done()
# as its explicitly testing what happens without it
with patch.dict(config_entries.HANDLERS, {"comp": TestFlow}):
assert await async_setup_component(hass, "comp", {})
assert len(async_setup_entry.mock_calls) == 1
assert load_events[0].event_type == EVENT_COMPONENT_LOADED
assert load_events[0].data == {"component": "comp"}
entries = hass.config_entries.async_entries("comp")
assert len(entries) == 1
assert entries[0].state is config_entries.ConfigEntryState.LOADED
listener()
async def test_async_setup_update_entry(hass):
"""Test a config entry being updated during integration setup."""
entry = MockConfigEntry(domain="comp", data={"value": "initial"})