Merge pull request #69509 from home-assistant/rc

This commit is contained in:
Paulus Schoutsen 2022-04-07 23:10:23 -07:00 committed by GitHub
commit 919f4dd719
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
63 changed files with 711 additions and 186 deletions

View File

@ -3,7 +3,7 @@
"name": "Airzone", "name": "Airzone",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/airzone", "documentation": "https://www.home-assistant.io/integrations/airzone",
"requirements": ["aioairzone==0.2.3"], "requirements": ["aioairzone==0.3.3"],
"codeowners": ["@Noltari"], "codeowners": ["@Noltari"],
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["aioairzone"] "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 # 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 # it back we now we are logged in either with or without a password
elk.add_handler("VN", first_response) 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) elk.add_handler("sync_complete", sync_complete)
for name, event, timeout in ( for name, event, timeout in (
("login", login_event, login_timeout), ("login", login_event, login_timeout),

View File

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

View File

@ -260,7 +260,7 @@ async def ws_get_fossil_energy_consumption(
statistic_ids.append(msg["co2_statistic_id"]) statistic_ids.append(msg["co2_statistic_id"])
# Fetch energy + CO2 statistics # 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, recorder.statistics.statistics_during_period,
hass, hass,
start_time, start_time,

View File

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

View File

@ -28,6 +28,7 @@
"abort": { "abort": {
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "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%]", "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.", "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%]" "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"

View File

@ -3,6 +3,7 @@
"abort": { "abort": {
"already_configured": "Device is already configured", "already_configured": "Device is already configured",
"already_in_progress": "Configuration flow is already in progress", "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", "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.", "not_supported": "Connected to AVM FRITZ!Box but it's unable to control Smart Home devices.",
"reauth_successful": "Re-authentication was successful" "reauth_successful": "Re-authentication was successful"

View File

@ -109,6 +109,20 @@ def build_schema(
return vol.Schema(spec) 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): def get_image_type(image):
"""Get the format of downloaded bytes that could be an image.""" """Get the format of downloaded bytes that could be an image."""
fmt = None 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.""" """Verify that the still image is valid before we create an entity."""
fmt = None fmt = None
if not (url := info.get(CONF_STILL_IMAGE_URL)): 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: if not isinstance(url, template_helper.Template) and url:
url = cv.template(url) url = cv.template(url)
url.hass = hass url.hass = hass
try: try:
url = url.async_render(parse_result=False) url = url.async_render(parse_result=False)
except TemplateError as err: 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 return {CONF_STILL_IMAGE_URL: "template_error"}, None
verify_ssl = info.get(CONF_VERIFY_SSL) verify_ssl = info.get(CONF_VERIFY_SSL)
auth = generate_auth(info) auth = generate_auth(info)
@ -228,6 +242,11 @@ class GenericIPCamConfigFlow(ConfigFlow, domain=DOMAIN):
VERSION = 1 VERSION = 1
def __init__(self):
"""Initialize Generic ConfigFlow."""
self.cached_user_input: dict[str, Any] = {}
self.cached_title = ""
@staticmethod @staticmethod
def async_get_options_flow( def async_get_options_flow(
config_entry: ConfigEntry, config_entry: ConfigEntry,
@ -238,8 +257,8 @@ class GenericIPCamConfigFlow(ConfigFlow, domain=DOMAIN):
def check_for_existing(self, options): def check_for_existing(self, options):
"""Check whether an existing entry is using the same URLs.""" """Check whether an existing entry is using the same URLs."""
return any( return any(
entry.options[CONF_STILL_IMAGE_URL] == options[CONF_STILL_IMAGE_URL] entry.options.get(CONF_STILL_IMAGE_URL) == options.get(CONF_STILL_IMAGE_URL)
and entry.options[CONF_STREAM_SOURCE] == options[CONF_STREAM_SOURCE] and entry.options.get(CONF_STREAM_SOURCE) == options.get(CONF_STREAM_SOURCE)
for entry in self._async_current_entries() for entry in self._async_current_entries()
) )
@ -264,10 +283,17 @@ class GenericIPCamConfigFlow(ConfigFlow, domain=DOMAIN):
if not errors: if not errors:
user_input[CONF_CONTENT_TYPE] = still_format user_input[CONF_CONTENT_TYPE] = still_format
user_input[CONF_LIMIT_REFETCH_TO_URL_CHANGE] = False user_input[CONF_LIMIT_REFETCH_TO_URL_CHANGE] = False
await self.async_set_unique_id(self.flow_id) if user_input.get(CONF_STILL_IMAGE_URL):
return self.async_create_entry( await self.async_set_unique_id(self.flow_id)
title=name, data={}, options=user_input 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: else:
user_input = DEFAULT_DATA.copy() user_input = DEFAULT_DATA.copy()
@ -277,12 +303,36 @@ class GenericIPCamConfigFlow(ConfigFlow, domain=DOMAIN):
errors=errors, 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: async def async_step_import(self, import_config) -> FlowResult:
"""Handle config import from yaml.""" """Handle config import from yaml."""
# abort if we've already got this one. # abort if we've already got this one.
if self.check_for_existing(import_config): if self.check_for_existing(import_config):
return self.async_abort(reason="already_exists") return self.async_abort(reason="already_exists")
errors, still_format = await async_test_still(self.hass, import_config) 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) errors = errors | await async_test_stream(self.hass, import_config)
still_url = import_config.get(CONF_STILL_IMAGE_URL) still_url = import_config.get(CONF_STILL_IMAGE_URL)
stream_url = import_config.get(CONF_STREAM_SOURCE) stream_url = import_config.get(CONF_STREAM_SOURCE)
@ -308,6 +358,8 @@ class GenericOptionsFlowHandler(OptionsFlow):
def __init__(self, config_entry: ConfigEntry) -> None: def __init__(self, config_entry: ConfigEntry) -> None:
"""Initialize Generic IP Camera options flow.""" """Initialize Generic IP Camera options flow."""
self.config_entry = config_entry self.config_entry = config_entry
self.cached_user_input: dict[str, Any] = {}
self.cached_title = ""
async def async_step_init( async def async_step_init(
self, user_input: dict[str, Any] | None = None self, user_input: dict[str, Any] | None = None
@ -316,29 +368,52 @@ class GenericOptionsFlowHandler(OptionsFlow):
errors: dict[str, str] = {} errors: dict[str, str] = {}
if user_input is not None: 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) errors = errors | await async_test_stream(self.hass, user_input)
still_url = user_input.get(CONF_STILL_IMAGE_URL) still_url = user_input.get(CONF_STILL_IMAGE_URL)
stream_url = user_input.get(CONF_STREAM_SOURCE) stream_url = user_input.get(CONF_STREAM_SOURCE)
if not errors: if not errors:
return self.async_create_entry( title = slug_url(still_url) or slug_url(stream_url) or DEFAULT_NAME
title=slug_url(still_url) or slug_url(stream_url) or DEFAULT_NAME, data = {
data={ CONF_AUTHENTICATION: user_input.get(CONF_AUTHENTICATION),
CONF_AUTHENTICATION: user_input.get(CONF_AUTHENTICATION), CONF_STREAM_SOURCE: user_input.get(CONF_STREAM_SOURCE),
CONF_STREAM_SOURCE: user_input.get(CONF_STREAM_SOURCE), CONF_PASSWORD: user_input.get(CONF_PASSWORD),
CONF_PASSWORD: user_input.get(CONF_PASSWORD), CONF_STILL_IMAGE_URL: user_input.get(CONF_STILL_IMAGE_URL),
CONF_STILL_IMAGE_URL: user_input.get(CONF_STILL_IMAGE_URL), CONF_CONTENT_TYPE: still_format
CONF_CONTENT_TYPE: still_format, or self.config_entry.options.get(CONF_CONTENT_TYPE),
CONF_USERNAME: user_input.get(CONF_USERNAME), CONF_USERNAME: user_input.get(CONF_USERNAME),
CONF_LIMIT_REFETCH_TO_URL_CHANGE: user_input[ CONF_LIMIT_REFETCH_TO_URL_CHANGE: user_input[
CONF_LIMIT_REFETCH_TO_URL_CHANGE CONF_LIMIT_REFETCH_TO_URL_CHANGE
], ],
CONF_FRAMERATE: user_input[CONF_FRAMERATE], CONF_FRAMERATE: user_input[CONF_FRAMERATE],
CONF_VERIFY_SSL: user_input[CONF_VERIFY_SSL], 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( return self.async_show_form(
step_id="init", step_id="init",
data_schema=build_schema(user_input or self.config_entry.options, True), data_schema=build_schema(user_input or self.config_entry.options, True),
errors=errors, 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", "limit_refetch_to_url_change": "Limit refetch to url change",
"password": "[%key:common::config_flow::data::password%]", "password": "[%key:common::config_flow::data::password%]",
"username": "[%key:common::config_flow::data::username%]", "username": "[%key:common::config_flow::data::username%]",
"content_type": "Content Type",
"framerate": "Frame Rate (Hz)", "framerate": "Frame Rate (Hz)",
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]" "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": { "confirm": {
"description": "[%key:common::config_flow::description::confirm_setup%]" "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%]", "limit_refetch_to_url_change": "[%key:component::generic::config::step::user::data::limit_refetch_to_url_change%]",
"password": "[%key:common::config_flow::data::password%]", "password": "[%key:common::config_flow::data::password%]",
"username": "[%key:common::config_flow::data::username%]", "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%]", "framerate": "[%key:component::generic::config::step::user::data::framerate%]",
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]" "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": { "error": {

View File

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

View File

@ -5,6 +5,7 @@ from __future__ import annotations
from collections.abc import Awaitable, Callable from collections.abc import Awaitable, Callable
import datetime import datetime
import logging import logging
import time
from typing import Any from typing import Any
from googleapiclient import discovery as google_discovery from googleapiclient import discovery as google_discovery
@ -58,7 +59,7 @@ class DeviceAuth(config_entry_oauth2_flow.LocalOAuth2Implementation):
"refresh_token": creds.refresh_token, "refresh_token": creds.refresh_token,
"scope": " ".join(creds.scopes), "scope": " ".join(creds.scopes),
"token_type": "Bearer", "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_id=conf[CONF_CLIENT_ID],
client_secret=conf[CONF_CLIENT_SECRET], client_secret=conf[CONF_CLIENT_SECRET],
refresh_token=token["refresh_token"], refresh_token=token["refresh_token"],
token_expiry=token["expires_at"], token_expiry=datetime.datetime.fromtimestamp(token["expires_at"]),
token_uri=oauth2client.GOOGLE_TOKEN_URI, token_uri=oauth2client.GOOGLE_TOKEN_URI,
scopes=[conf[CONF_CALENDAR_ACCESS].scope], scopes=[conf[CONF_CALENDAR_ACCESS].scope],
user_agent=None, 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.""" """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: class GoogleCalendarService:

View File

@ -183,7 +183,9 @@ class GoogleCalendarEventDevice(CalendarEventDevice):
valid_items = filter(self._event_filter, items) valid_items = filter(self._event_filter, items)
self._event = copy.deepcopy(next(valid_items, None)) self._event = copy.deepcopy(next(valid_items, None))
if self._event: 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._event["summary"] = summary
self._offset_reached = is_offset_reached( self._offset_reached = is_offset_reached(
get_date(self._event["start"]), offset get_date(self._event["start"]), offset

View File

@ -824,7 +824,7 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator):
self.data = {} self.data = {}
self.entry_id = config_entry.entry_id self.entry_id = config_entry.entry_id
self.dev_reg = dev_reg 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]: async def _async_update_data(self) -> dict[str, Any]:
"""Update data via library.""" """Update data via library."""
@ -891,6 +891,12 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator):
if stale_addons := supervisor_addon_devices - set(new_data[DATA_KEY_ADDONS]): if stale_addons := supervisor_addon_devices - set(new_data[DATA_KEY_ADDONS]):
async_remove_addons_from_dev_reg(self.dev_reg, stale_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 # 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 # create new devices and entities. We can return an empty dict because
# coordinator will be recreated. # coordinator will be recreated.

View File

@ -652,7 +652,7 @@ def _exclude_by_entity_registry(
(entry := ent_reg.async_get(entity_id)) (entry := ent_reg.async_get(entity_id))
and ( and (
entry.hidden_by is not None 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: if self._dt_diff_seconds > 5:
LOGGER.warning( 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', " "which is different from the system '%s', "
"this could lead to authentication issues", "this could lead to authentication issues",
self.name,
cam_date_utc, cam_date_utc,
system_date, 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: def _fetch_powerwall_data(power_wall: Powerwall) -> PowerwallData:
"""Process and update powerwall data.""" """Process and update powerwall data."""
try:
backup_reserve = power_wall.get_backup_reserve_percentage()
except MissingAttributeError:
backup_reserve = None
return PowerwallData( return PowerwallData(
charge=power_wall.get_charge(), charge=power_wall.get_charge(),
site_master=power_wall.get_sitemaster(), site_master=power_wall.get_sitemaster(),
meters=power_wall.get_meters(), meters=power_wall.get_meters(),
grid_services_active=power_wall.is_grid_services_active(), grid_services_active=power_wall.is_grid_services_active(),
grid_status=power_wall.get_grid_status(), 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 meters: MetersAggregates
grid_services_active: bool grid_services_active: bool
grid_status: GridStatus grid_status: GridStatus
backup_reserve: float backup_reserve: float | None
class PowerwallRuntimeData(TypedDict): class PowerwallRuntimeData(TypedDict):

View File

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

View File

@ -2,8 +2,8 @@
"domain": "remote_rpi_gpio", "domain": "remote_rpi_gpio",
"name": "remote_rpi_gpio", "name": "remote_rpi_gpio",
"documentation": "https://www.home-assistant.io/integrations/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": [], "codeowners": [],
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["gpiozero"] "loggers": ["gpiozero", "pigpio"]
} }

View File

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

View File

@ -18,7 +18,10 @@ from homeassistant.util import Throttle
from .const import ( from .const import (
CONF_FALLBACK, CONF_FALLBACK,
CONST_OVERLAY_MANUAL,
CONST_OVERLAY_TADO_DEFAULT,
CONST_OVERLAY_TADO_MODE, CONST_OVERLAY_TADO_MODE,
CONST_OVERLAY_TADO_OPTIONS,
DATA, DATA,
DOMAIN, DOMAIN,
INSIDE_TEMPERATURE_MEASUREMENT, INSIDE_TEMPERATURE_MEASUREMENT,
@ -51,7 +54,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
username = entry.data[CONF_USERNAME] username = entry.data[CONF_USERNAME]
password = entry.data[CONF_PASSWORD] 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) 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): def _async_import_options_from_data_if_missing(hass: HomeAssistant, entry: ConfigEntry):
options = dict(entry.options) options = dict(entry.options)
if CONF_FALLBACK not in 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) 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.core import callback
from homeassistant.data_entry_flow import FlowResult 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__) _LOGGER = logging.getLogger(__name__)
@ -126,7 +132,10 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
data_schema = vol.Schema( data_schema = vol.Schema(
{ {
vol.Optional( 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), ): vol.In(CONST_OVERLAY_TADO_OPTIONS),
} }
) )

View File

@ -1,17 +1,6 @@
"""Support for Telegram bot to send messages only.""" """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.""" """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 return True

View File

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

View File

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

View File

@ -116,9 +116,6 @@ class ProtectFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
self.hass.config_entries.async_update_entry( self.hass.config_entries.async_update_entry(
entry, data={**entry.data, CONF_HOST: new_host} 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") return self.async_abort(reason="already_configured")
if entry_host in (direct_connect_domain, source_ip) or ( if entry_host in (direct_connect_domain, source_ip) or (
entry_has_direct_connect 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): async def async_reset_meters(service_call):
"""Reset all sensors of a meter.""" """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] for meter in meters:
if domain == DOMAIN: _LOGGER.debug("resetting meter %s", meter)
for entity in hass.data[DATA_LEGACY_COMPONENT].entities: domain, entity = split_entity_id(meter)
if entity_id == entity.entity_id: # backward compatibility up to 2022.07:
_LOGGER.debug( if domain == DOMAIN:
"forward reset meter from %s to %s", async_dispatcher_send(
entity_id, hass, SIGNAL_RESET_METER, f"{SELECT_DOMAIN}.{entity}"
entity.tracked_entity_id, )
) else:
entity_id = entity.tracked_entity_id async_dispatcher_send(hass, SIGNAL_RESET_METER, meter)
_LOGGER.debug("reset meter %s", entity_id)
async_dispatcher_send(hass, SIGNAL_RESET_METER, entity_id)
hass.services.async_register( hass.services.async_register(
DOMAIN, DOMAIN,
SERVICE_RESET, SERVICE_RESET,
async_reset_meters, 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: if DOMAIN not in config:

View File

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

View File

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

View File

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

View File

@ -8,7 +8,12 @@ import logging
from typing import Any from typing import Any
import zigpy.exceptions 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.const import ATTR_COMMAND
from homeassistant.core import callback from homeassistant.core import callback
@ -20,6 +25,7 @@ from ..const import (
ATTR_ATTRIBUTE_ID, ATTR_ATTRIBUTE_ID,
ATTR_ATTRIBUTE_NAME, ATTR_ATTRIBUTE_NAME,
ATTR_CLUSTER_ID, ATTR_CLUSTER_ID,
ATTR_PARAMS,
ATTR_TYPE, ATTR_TYPE,
ATTR_UNIQUE_ID, ATTR_UNIQUE_ID,
ATTR_VALUE, ATTR_VALUE,
@ -111,7 +117,11 @@ class ZigbeeChannel(LogMixin):
if not hasattr(self, "_value_attribute") and self.REPORT_CONFIG: if not hasattr(self, "_value_attribute") and self.REPORT_CONFIG:
attr = self.REPORT_CONFIG[0].get("attr") attr = self.REPORT_CONFIG[0].get("attr")
if isinstance(attr, str): 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: else:
self.value_attribute = attr self.value_attribute = attr
self._status = ChannelStatus.CREATED self._status = ChannelStatus.CREATED
@ -354,14 +364,27 @@ class ZigbeeChannel(LogMixin):
"""Handle ZDO commands on this cluster.""" """Handle ZDO commands on this cluster."""
@callback @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.""" """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( self._ch_pool.zha_send_event(
{ {
ATTR_UNIQUE_ID: self.unique_id, ATTR_UNIQUE_ID: self.unique_id,
ATTR_CLUSTER_ID: self.cluster.cluster_id, ATTR_CLUSTER_ID: self.cluster.cluster_id,
ATTR_COMMAND: command, ATTR_COMMAND: command,
# Maintain backwards compatibility with the old zigpy response format
ATTR_ARGS: args, ATTR_ARGS: args,
ATTR_PARAMS: params,
} }
) )

View File

@ -43,6 +43,7 @@ ATTR_NEIGHBORS = "neighbors"
ATTR_NODE_DESCRIPTOR = "node_descriptor" ATTR_NODE_DESCRIPTOR = "node_descriptor"
ATTR_NWK = "nwk" ATTR_NWK = "nwk"
ATTR_OUT_CLUSTERS = "out_clusters" ATTR_OUT_CLUSTERS = "out_clusters"
ATTR_PARAMS = "params"
ATTR_POWER_SOURCE = "power_source" ATTR_POWER_SOURCE = "power_source"
ATTR_PROFILE_ID = "profile_id" ATTR_PROFILE_ID = "profile_id"
ATTR_QUIRK_APPLIED = "quirk_applied" 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: async def async_add_to_group(self, group_id: int) -> None:
"""Add this device to the provided zigbee group.""" """Add this device to the provided zigbee group."""
try: 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: except (zigpy.exceptions.ZigbeeException, asyncio.TimeoutError) as ex:
self.debug( self.debug(
"Failed to add device '%s' to group: 0x%04x ex: %s", "Failed to add device '%s' to group: 0x%04x ex: %s",
@ -687,7 +691,9 @@ class ZHADevice(LogMixin):
) -> None: ) -> None:
"""Add the device endpoint to the provided zigbee group.""" """Add the device endpoint to the provided zigbee group."""
try: 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: except (zigpy.exceptions.ZigbeeException, asyncio.TimeoutError) as ex:
self.debug( self.debug(
"Failed to add endpoint: %s for device: '%s' to group: 0x%04x ex: %s", "Failed to add endpoint: %s for device: '%s' to group: 0x%04x ex: %s",

View File

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

View File

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

View File

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

View File

@ -7,7 +7,7 @@ from .backports.enum import StrEnum
MAJOR_VERSION: Final = 2022 MAJOR_VERSION: Final = 2022
MINOR_VERSION: Final = 4 MINOR_VERSION: Final = 4
PATCH_VERSION: Final = "0" PATCH_VERSION: Final = "1"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) 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 aio_georss_gdacs==0.5
# homeassistant.components.airzone # homeassistant.components.airzone
aioairzone==0.2.3 aioairzone==0.3.3
# homeassistant.components.ambient_station # homeassistant.components.ambient_station
aioambient==2021.11.0 aioambient==2021.11.0
@ -740,7 +740,7 @@ googlemaps==2.5.1
goslide-api==0.5.1 goslide-api==0.5.1
# homeassistant.components.remote_rpi_gpio # homeassistant.components.remote_rpi_gpio
gpiozero==1.5.1 gpiozero==1.6.2
# homeassistant.components.gpsd # homeassistant.components.gpsd
gps3==0.33.3 gps3==0.33.3
@ -1198,6 +1198,9 @@ phone_modem==0.1.1
# homeassistant.components.onewire # homeassistant.components.onewire
pi1wire==0.1.0 pi1wire==0.1.0
# homeassistant.components.remote_rpi_gpio
pigpio==1.78
# homeassistant.components.pilight # homeassistant.components.pilight
pilight==0.1.1 pilight==0.1.1
@ -1511,7 +1514,7 @@ pygtfs==0.1.6
pygti==0.9.2 pygti==0.9.2
# homeassistant.components.version # homeassistant.components.version
pyhaversion==22.04.0 pyhaversion==22.4.1
# homeassistant.components.heos # homeassistant.components.heos
pyheos==0.7.2 pyheos==0.7.2
@ -2470,7 +2473,7 @@ zengge==0.2
zeroconf==0.38.4 zeroconf==0.38.4
# homeassistant.components.zha # homeassistant.components.zha
zha-quirks==0.0.69 zha-quirks==0.0.71
# homeassistant.components.zhong_hong # homeassistant.components.zhong_hong
zhong_hong_hvac==1.0.9 zhong_hong_hvac==1.0.9

View File

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

View File

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

View File

@ -2,7 +2,7 @@
from unittest.mock import MagicMock, patch 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 import data_entry_flow
from homeassistant.components.airzone.const import DOMAIN from homeassistant.components.airzone.const import DOMAIN
@ -23,6 +23,12 @@ async def test_form(hass):
) as mock_setup_entry, patch( ) as mock_setup_entry, patch(
"homeassistant.components.airzone.AirzoneLocalApi.get_hvac", "homeassistant.components.airzone.AirzoneLocalApi.get_hvac",
return_value=HVAC_MOCK, 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( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER} DOMAIN, context={"source": SOURCE_USER}

View File

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

View File

@ -2,6 +2,7 @@
import dataclasses import dataclasses
from unittest import mock from unittest import mock
from unittest.mock import Mock, patch from unittest.mock import Mock, patch
from urllib.parse import urlparse
from pyfritzhome import LoginError from pyfritzhome import LoginError
import pytest import pytest
@ -24,15 +25,35 @@ from .const import CONF_FAKE_NAME, MOCK_CONFIG
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
MOCK_USER_DATA = MOCK_CONFIG[DOMAIN][CONF_DEVICES][0] MOCK_USER_DATA = MOCK_CONFIG[DOMAIN][CONF_DEVICES][0]
MOCK_SSDP_DATA = ssdp.SsdpServiceInfo( MOCK_SSDP_DATA = {
ssdp_usn="mock_usn", "ip4_valid": ssdp.SsdpServiceInfo(
ssdp_st="mock_st", ssdp_usn="mock_usn",
ssdp_location="https://fake_host:12345/test", ssdp_st="mock_st",
upnp={ ssdp_location="https://10.0.0.1:12345/test",
ATTR_UPNP_FRIENDLY_NAME: CONF_FAKE_NAME, upnp={
ATTR_UPNP_UDN: "uuid:only-a-test", 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") @pytest.fixture(name="fritz")
@ -56,8 +77,8 @@ async def test_user(hass: HomeAssistant, fritz: Mock):
result["flow_id"], user_input=MOCK_USER_DATA result["flow_id"], user_input=MOCK_USER_DATA
) )
assert result["type"] == RESULT_TYPE_CREATE_ENTRY assert result["type"] == RESULT_TYPE_CREATE_ENTRY
assert result["title"] == "fake_host" assert result["title"] == "10.0.0.1"
assert result["data"][CONF_HOST] == "fake_host" assert result["data"][CONF_HOST] == "10.0.0.1"
assert result["data"][CONF_PASSWORD] == "fake_pass" assert result["data"][CONF_PASSWORD] == "fake_pass"
assert result["data"][CONF_USERNAME] == "fake_user" assert result["data"][CONF_USERNAME] == "fake_user"
assert not result["result"].unique_id 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" 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.""" """Test starting a flow from discovery."""
result = await hass.config_entries.flow.async_init( 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" assert result["step_id"] == "confirm"
result = await hass.config_entries.flow.async_configure( 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["type"] == RESULT_TYPE_CREATE_ENTRY
assert result["title"] == CONF_FAKE_NAME 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_PASSWORD] == "fake_pass"
assert result["data"][CONF_USERNAME] == "fake_user" assert result["data"][CONF_USERNAME] == "fake_user"
assert result["result"].unique_id == "only-a-test" 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): async def test_ssdp_no_friendly_name(hass: HomeAssistant, fritz: Mock):
"""Test starting a flow from discovery without friendly name.""" """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() MOCK_NO_NAME.upnp = MOCK_NO_NAME.upnp.copy()
del MOCK_NO_NAME.upnp[ATTR_UPNP_FRIENDLY_NAME] del MOCK_NO_NAME.upnp[ATTR_UPNP_FRIENDLY_NAME]
result = await hass.config_entries.flow.async_init( 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"}, user_input={CONF_PASSWORD: "fake_pass", CONF_USERNAME: "fake_user"},
) )
assert result["type"] == RESULT_TYPE_CREATE_ENTRY assert result["type"] == RESULT_TYPE_CREATE_ENTRY
assert result["title"] == "fake_host" assert result["title"] == "10.0.0.1"
assert result["data"][CONF_HOST] == "fake_host" assert result["data"][CONF_HOST] == "10.0.0.1"
assert result["data"][CONF_PASSWORD] == "fake_pass" assert result["data"][CONF_PASSWORD] == "fake_pass"
assert result["data"][CONF_USERNAME] == "fake_user" assert result["data"][CONF_USERNAME] == "fake_user"
assert result["result"].unique_id == "only-a-test" 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") fritz().login.side_effect = LoginError("Boom")
result = await hass.config_entries.flow.async_init( 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["type"] == RESULT_TYPE_FORM
assert result["step_id"] == "confirm" 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") fritz().login.side_effect = OSError("Boom")
result = await hass.config_entries.flow.async_init( 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["type"] == RESULT_TYPE_FORM
assert result["step_id"] == "confirm" 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") fritz().get_device_elements.side_effect = HTTPError("Boom")
result = await hass.config_entries.flow.async_init( 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["type"] == RESULT_TYPE_FORM
assert result["step_id"] == "confirm" 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): async def test_ssdp_already_in_progress_unique_id(hass: HomeAssistant, fritz: Mock):
"""Test starting a flow from discovery twice.""" """Test starting a flow from discovery twice."""
result = await hass.config_entries.flow.async_init( 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["type"] == RESULT_TYPE_FORM
assert result["step_id"] == "confirm" assert result["step_id"] == "confirm"
result = await hass.config_entries.flow.async_init( 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["type"] == RESULT_TYPE_ABORT
assert result["reason"] == "already_in_progress" 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): async def test_ssdp_already_in_progress_host(hass: HomeAssistant, fritz: Mock):
"""Test starting a flow from discovery twice.""" """Test starting a flow from discovery twice."""
result = await hass.config_entries.flow.async_init( 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["type"] == RESULT_TYPE_FORM
assert result["step_id"] == "confirm" 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() MOCK_NO_UNIQUE_ID.upnp = MOCK_NO_UNIQUE_ID.upnp.copy()
del MOCK_NO_UNIQUE_ID.upnp[ATTR_UPNP_UDN] del MOCK_NO_UNIQUE_ID.upnp[ATTR_UPNP_UDN]
result = await hass.config_entries.flow.async_init( 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 assert not result["result"].unique_id
result2 = await hass.config_entries.flow.async_init( 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["type"] == RESULT_TYPE_ABORT
assert result2["reason"] == "already_configured" 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() entries = hass.config_entries.async_entries()
assert entries assert entries
assert len(entries) == 1 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_PASSWORD] == "fake_pass"
assert entries[0].data[CONF_USERNAME] == "fake_user" assert entries[0].data[CONF_USERNAME] == "fake_user"
assert fritz.call_count == 1 assert fritz.call_count == 1
assert fritz.call_args_list == [ 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 import respx
from homeassistant import config_entries, data_entry_flow, setup from homeassistant import config_entries, data_entry_flow, setup
from homeassistant.components.camera import async_get_image
from homeassistant.components.generic.const import ( from homeassistant.components.generic.const import (
CONF_CONTENT_TYPE, CONF_CONTENT_TYPE,
CONF_FRAMERATE, 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 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.""" """Test we complete ok if the user wants stream only."""
await setup.async_setup_component(hass, "persistent_notification", {}) await setup.async_setup_component(hass, "persistent_notification", {})
result = await hass.config_entries.flow.async_init( 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"], result["flow_id"],
data, data,
) )
assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result2["title"] == "127_0_0_1_testurl_2" result3 = await hass.config_entries.flow.async_configure(
assert result2["options"] == { 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_AUTHENTICATION: HTTP_BASIC_AUTHENTICATION,
CONF_STREAM_SOURCE: "http://127.0.0.1/testurl/2", CONF_STREAM_SOURCE: "http://127.0.0.1/testurl/2",
CONF_RTSP_TRANSPORT: "tcp", CONF_RTSP_TRANSPORT: "tcp",
CONF_USERNAME: "fred_flintstone", CONF_USERNAME: "fred_flintstone",
CONF_PASSWORD: "bambam", CONF_PASSWORD: "bambam",
CONF_LIMIT_REFETCH_TO_URL_CHANGE: False, CONF_LIMIT_REFETCH_TO_URL_CHANGE: False,
CONF_CONTENT_TYPE: None, CONF_CONTENT_TYPE: "image/jpeg",
CONF_FRAMERATE: 5, CONF_FRAMERATE: 5,
CONF_VERIFY_SSL: False, CONF_VERIFY_SSL: False,
} }
await hass.async_block_till_done() 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 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"} 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. # These below can be deleted after deprecation period is finished.
@respx.mock @respx.mock
async def test_import(hass, fakeimg_png, mock_av_open): 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( async def test_update_error(
hass, calendar_resource, component_setup, test_api_calendar hass, calendar_resource, component_setup, test_api_calendar
): ):

View File

@ -97,6 +97,12 @@ async def test_full_flow(
assert "data" in result assert "data" in result
data = result["data"] data = result["data"]
assert "token" in 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_at")
data["token"].pop("expires_in") data["token"].pop("expires_in")
assert data == { assert data == {

View File

@ -24,7 +24,11 @@ def mock_all(aioclient_mock, request):
"http://127.0.0.1/info", "http://127.0.0.1/info",
json={ json={
"result": "ok", "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( aioclient_mock.get(

View File

@ -30,7 +30,11 @@ def mock_all(aioclient_mock, request):
"http://127.0.0.1/info", "http://127.0.0.1/info",
json={ json={
"result": "ok", "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( aioclient_mock.get(
@ -396,14 +400,14 @@ async def test_service_calls(hassio_env, hass, aioclient_mock, caplog):
) )
await hass.async_block_till_done() 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" assert aioclient_mock.mock_calls[-1][2] == "test"
await hass.services.async_call("hassio", "host_shutdown", {}) await hass.services.async_call("hassio", "host_shutdown", {})
await hass.services.async_call("hassio", "host_reboot", {}) await hass.services.async_call("hassio", "host_reboot", {})
await hass.async_block_till_done() 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("hassio", "backup_full", {})
await hass.services.async_call( 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() 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] == { assert aioclient_mock.mock_calls[-1][2] == {
"homeassistant": True, "homeassistant": True,
"addons": ["test"], "addons": ["test"],
@ -442,7 +446,7 @@ async def test_service_calls(hassio_env, hass, aioclient_mock, caplog):
) )
await hass.async_block_till_done() 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] == { assert aioclient_mock.mock_calls[-1][2] == {
"addons": ["test"], "addons": ["test"],
"folders": ["ssl"], "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.services.async_call("homeassistant", "stop")
await hass.async_block_till_done() 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.services.async_call("homeassistant", "check_config")
await hass.async_block_till_done() await hass.async_block_till_done()
assert aioclient_mock.call_count == 4 assert aioclient_mock.call_count == 5
with patch( with patch(
"homeassistant.config.async_check_ha_config_file", return_value=None "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() await hass.async_block_till_done()
assert mock_check_config.called 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): async def test_entry_load_and_unload(hass):
@ -628,10 +632,17 @@ async def test_device_registry_calls(hass):
), patch( ), patch(
"homeassistant.components.hassio.HassIO.get_os_info", "homeassistant.components.hassio.HassIO.get_os_info",
return_value=os_mock_data, 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)) async_fire_time_changed(hass, dt_util.now() + timedelta(hours=3))
await hass.async_block_till_done() 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): 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", "http://127.0.0.1/info",
json={ json={
"result": "ok", "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( aioclient_mock.get(

View File

@ -25,7 +25,11 @@ def mock_all(aioclient_mock, request):
"http://127.0.0.1/info", "http://127.0.0.1/info",
json={ json={
"result": "ok", "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( aioclient_mock.get(
@ -483,3 +487,25 @@ async def test_not_release_notes(hass, aioclient_mock, hass_ws_client):
) )
result = await client.receive_json() result = await client.receive_json()
assert result["result"] is None 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, entity_category=EntityCategory.CONFIG,
) )
hass.states.async_set(sonos_config_switch.entity_id, "off") 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() await hass.async_block_till_done()
result = await hass.config_entries.options.async_init( 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( result4 = await hass.config_entries.options.async_configure(
result2["flow_id"], 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 result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert config_entry.options == { assert config_entry.options == {
"mode": "bridge", "mode": "bridge",
"filter": { "filter": {
"exclude_domains": [], "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_domains": ["media_player", "switch"],
"include_entities": [], "include_entities": [],
}, },

View File

@ -1,5 +1,7 @@
"""The sensor tests for the powerwall platform.""" """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.powerwall.const import DOMAIN
from homeassistant.components.sensor import ATTR_STATE_CLASS 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 # HA changes the implementation and a new one appears
for key, value in expected_attributes.items(): for key, value in expected_attributes.items():
assert state.attributes[key] == value 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 from unittest.mock import patch
import pytest import pytest
from telegram.ext.dispatcher import Dispatcher
from homeassistant.components.telegram_bot import ( from homeassistant.components.telegram_bot import (
CONF_ALLOWED_CHAT_IDS, CONF_ALLOWED_CHAT_IDS,
@ -176,12 +175,3 @@ async def polling_platform(hass, config_polling):
config_polling, config_polling,
) )
await hass.async_block_till_done() 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.""" """Tests for the telegram_bot component."""
import pytest
from telegram import Update from telegram import Update
from telegram.ext.dispatcher import Dispatcher 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 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): async def test_webhook_platform_init(hass, webhook_platform):
"""Test initialization of the webhooks platform.""" """Test initialization of the webhooks platform."""
assert hass.services.has_service(DOMAIN, SERVICE_SEND_MESSAGE) is True assert hass.services.has_service(DOMAIN, SERVICE_SEND_MESSAGE) is True

View File

@ -517,6 +517,33 @@ async def test_smart_strip_custom_random_effect(hass: HomeAssistant) -> None:
) )
strip.set_custom_effect.reset_mock() 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 = { strip.effect = {
"custom": 1, "custom": 1,
"id": "yMwcNpLxijmoKamskHCvvravpbnIqAIN", "id": "yMwcNpLxijmoKamskHCvvravpbnIqAIN",

View File

@ -365,10 +365,7 @@ async def test_discovered_by_unifi_discovery_direct_connect_updated(
) )
mock_config.add_to_hass(hass) mock_config.add_to_hass(hass)
with _patch_discovery(), patch( with _patch_discovery():
"homeassistant.components.unifiprotect.async_setup_entry",
return_value=True,
) as mock_setup_entry:
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, DOMAIN,
context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, 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["type"] == RESULT_TYPE_ABORT
assert result["reason"] == "already_configured" assert result["reason"] == "already_configured"
assert len(mock_setup_entry.mock_calls) == 1
assert mock_config.data[CONF_HOST] == DIRECT_CONNECT_DOMAIN 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) mock_config.add_to_hass(hass)
with _patch_discovery(), patch( with _patch_discovery():
"homeassistant.components.unifiprotect.async_setup_entry",
return_value=True,
) as mock_setup_entry:
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, DOMAIN,
context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, 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["type"] == RESULT_TYPE_ABORT
assert result["reason"] == "already_configured" assert result["reason"] == "already_configured"
assert len(mock_setup_entry.mock_calls) == 1
assert mock_config.data[CONF_HOST] == "127.0.0.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" 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.""" """Test energy sensor reset service."""
config = { config = {
"utility_meter": { "utility_meter": {
@ -159,7 +168,7 @@ async def test_services(hass):
assert state.state == "1" assert state.state == "1"
# Reset meters # 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.services.async_call(DOMAIN, SERVICE_RESET, data)
await hass.async_block_till_done() await hass.async_block_till_done()

View File

@ -140,3 +140,13 @@ async def test_action(hass, device_ias):
assert calls[0].domain == DOMAIN assert calls[0].domain == DOMAIN
assert calls[0].service == "warning_device_warn" assert calls[0].service == "warning_device_warn"
assert calls[0].data["ieee"] == ieee_address 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.", "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.", "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.)", "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 "isEmbedded": true
}, },

View File

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

View File

@ -8,8 +8,12 @@ import pytest
from homeassistant import config_entries, data_entry_flow, loader from homeassistant import config_entries, data_entry_flow, loader
from homeassistant.components.hassio import HassioServiceInfo from homeassistant.components.hassio import HassioServiceInfo
from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP from homeassistant.const import (
from homeassistant.core import CoreState, callback 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.data_entry_flow import RESULT_TYPE_ABORT, BaseServiceInfo
from homeassistant.exceptions import ( from homeassistant.exceptions import (
ConfigEntryAuthFailed, ConfigEntryAuthFailed,
@ -2299,6 +2303,72 @@ async def test_async_setup_init_entry(hass):
assert entries[0].state is config_entries.ConfigEntryState.LOADED 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): async def test_async_setup_update_entry(hass):
"""Test a config entry being updated during integration setup.""" """Test a config entry being updated during integration setup."""
entry = MockConfigEntry(domain="comp", data={"value": "initial"}) entry = MockConfigEntry(domain="comp", data={"value": "initial"})