Merge pull request #75528 from home-assistant/rc

This commit is contained in:
Franck Nijhof 2022-07-20 22:52:11 +02:00 committed by GitHub
commit cd0656bab0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
45 changed files with 1170 additions and 163 deletions

View File

@ -4,7 +4,7 @@ from __future__ import annotations
from collections.abc import Mapping
from datetime import timedelta
from math import ceil
from typing import Any, cast
from typing import Any
from pyairvisual import CloudAPI, NodeSamba
from pyairvisual.errors import (
@ -12,6 +12,7 @@ from pyairvisual.errors import (
InvalidKeyError,
KeyExpiredError,
NodeProError,
UnauthorizedError,
)
from homeassistant.config_entries import ConfigEntry
@ -210,9 +211,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
)
try:
data = await api_coro
return cast(dict[str, Any], data)
except (InvalidKeyError, KeyExpiredError) as ex:
return await api_coro
except (InvalidKeyError, KeyExpiredError, UnauthorizedError) as ex:
raise ConfigEntryAuthFailed from ex
except AirVisualError as err:
raise UpdateFailed(f"Error while retrieving data: {err}") from err
@ -253,8 +253,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async with NodeSamba(
entry.data[CONF_IP_ADDRESS], entry.data[CONF_PASSWORD]
) as node:
data = await node.async_get_latest_measurements()
return cast(dict[str, Any], data)
return await node.async_get_latest_measurements()
except NodeProError as err:
raise UpdateFailed(f"Error while retrieving data: {err}") from err

View File

@ -9,8 +9,10 @@ from pyairvisual import CloudAPI, NodeSamba
from pyairvisual.errors import (
AirVisualError,
InvalidKeyError,
KeyExpiredError,
NodeProError,
NotFoundError,
UnauthorizedError,
)
import voluptuous as vol
@ -119,7 +121,7 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
if user_input[CONF_API_KEY] not in valid_keys:
try:
await coro
except InvalidKeyError:
except (InvalidKeyError, KeyExpiredError, UnauthorizedError):
errors[CONF_API_KEY] = "invalid_api_key"
except NotFoundError:
errors[CONF_CITY] = "location_not_found"

View File

@ -3,7 +3,7 @@
"name": "AirVisual",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/airvisual",
"requirements": ["pyairvisual==5.0.9"],
"requirements": ["pyairvisual==2022.07.0"],
"codeowners": ["@bachya"],
"iot_class": "cloud_polling",
"loggers": ["pyairvisual", "pysmb"]

View File

@ -88,6 +88,7 @@ class AladdinDevice(CoverEntity):
self._device_id = device["device_id"]
self._number = device["door_number"]
self._attr_name = device["name"]
self._serial = device["serial"]
self._attr_unique_id = f"{self._device_id}-{self._number}"
async def async_added_to_hass(self) -> None:
@ -97,8 +98,8 @@ class AladdinDevice(CoverEntity):
"""Schedule a state update."""
self.async_write_ha_state()
self._acc.register_callback(update_callback, self._number)
await self._acc.get_doors(self._number)
self._acc.register_callback(update_callback, self._serial)
await self._acc.get_doors(self._serial)
async def async_will_remove_from_hass(self) -> None:
"""Close Aladdin Connect before removing."""
@ -114,7 +115,7 @@ class AladdinDevice(CoverEntity):
async def async_update(self) -> None:
"""Update status of cover."""
await self._acc.get_doors(self._number)
await self._acc.get_doors(self._serial)
@property
def is_closed(self) -> bool | None:

View File

@ -2,7 +2,7 @@
"domain": "aladdin_connect",
"name": "Aladdin Connect",
"documentation": "https://www.home-assistant.io/integrations/aladdin_connect",
"requirements": ["AIOAladdinConnect==0.1.25"],
"requirements": ["AIOAladdinConnect==0.1.27"],
"codeowners": ["@mkmer"],
"iot_class": "cloud_polling",
"loggers": ["aladdin_connect"],

View File

@ -11,3 +11,4 @@ class DoorDevice(TypedDict):
door_number: int
name: str
status: str
serial: str

View File

@ -281,14 +281,14 @@ SENSOR_DESCRIPTIONS = (
name="Lightning Strikes Per Day",
icon="mdi:lightning-bolt",
native_unit_of_measurement="strikes",
state_class=SensorStateClass.TOTAL_INCREASING,
state_class=SensorStateClass.TOTAL,
),
SensorEntityDescription(
key=TYPE_LIGHTNING_PER_HOUR,
name="Lightning Strikes Per Hour",
icon="mdi:lightning-bolt",
native_unit_of_measurement="strikes",
state_class=SensorStateClass.TOTAL_INCREASING,
state_class=SensorStateClass.TOTAL,
),
SensorEntityDescription(
key=TYPE_MAXDAILYGUST,

View File

@ -87,7 +87,7 @@ class ArubaDeviceScanner(DeviceScanner):
def get_aruba_data(self):
"""Retrieve data from Aruba Access Point and return parsed result."""
connect = f"ssh {self.username}@{self.host}"
connect = f"ssh {self.username}@{self.host} -o HostKeyAlgorithms=ssh-rsa"
ssh = pexpect.spawn(connect)
query = ssh.expect(
[

View File

@ -33,7 +33,8 @@ class BMWDataUpdateCoordinator(DataUpdateCoordinator):
entry.data[CONF_PASSWORD],
get_region_from_name(entry.data[CONF_REGION]),
observer_position=GPSPosition(hass.config.latitude, hass.config.longitude),
use_metric_units=hass.config.units.is_metric,
# Force metric system as BMW API apparently only returns metric values now
use_metric_units=True,
)
self.read_only = entry.options[CONF_READ_ONLY]
self._entry = entry

View File

@ -2,7 +2,7 @@
"domain": "bmw_connected_drive",
"name": "BMW Connected Drive",
"documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive",
"requirements": ["bimmer_connected==0.9.6"],
"requirements": ["bimmer_connected==0.10.1"],
"codeowners": ["@gerard33", "@rikroe"],
"config_flow": true,
"iot_class": "cloud_polling",

View File

@ -16,7 +16,6 @@ from homeassistant.components.sensor import (
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONF_UNIT_SYSTEM_IMPERIAL,
LENGTH_KILOMETERS,
LENGTH_MILES,
PERCENTAGE,
@ -183,9 +182,7 @@ class BMWSensor(BMWBaseEntity, SensorEntity):
self._attr_name = f"{vehicle.name} {description.key}"
self._attr_unique_id = f"{vehicle.vin}-{description.key}"
if unit_system.name == CONF_UNIT_SYSTEM_IMPERIAL:
self._attr_native_unit_of_measurement = description.unit_imperial
else:
# Force metric system as BMW API apparently only returns metric values now
self._attr_native_unit_of_measurement = description.unit_metric
@callback

View File

@ -99,7 +99,7 @@ class ForecastSolarOptionFlowHandler(OptionsFlow):
CONF_API_KEY,
description={
"suggested_value": self.config_entry.options.get(
CONF_API_KEY
CONF_API_KEY, ""
)
},
): str,

View File

@ -44,13 +44,15 @@ class HiveDeviceLight(HiveEntity, LightEntity):
super().__init__(hive, hive_device)
if self.device["hiveType"] == "warmwhitelight":
self._attr_supported_color_modes = {ColorMode.BRIGHTNESS}
self._attr_color_mode = ColorMode.BRIGHTNESS
elif self.device["hiveType"] == "tuneablelight":
self._attr_supported_color_modes = {ColorMode.COLOR_TEMP}
self._attr_color_mode = ColorMode.COLOR_TEMP
elif self.device["hiveType"] == "colourtuneablelight":
self._attr_supported_color_modes = {ColorMode.COLOR_TEMP, ColorMode.HS}
self._attr_min_mireds = self.device.get("min_mireds")
self._attr_max_mireds = self.device.get("max_mireds")
self._attr_min_mireds = 153
self._attr_max_mireds = 370
@refresh_system
async def async_turn_on(self, **kwargs):
@ -94,6 +96,13 @@ class HiveDeviceLight(HiveEntity, LightEntity):
if self._attr_available:
self._attr_is_on = self.device["status"]["state"]
self._attr_brightness = self.device["status"]["brightness"]
if self.device["hiveType"] == "tuneablelight":
self._attr_color_temp = self.device["status"].get("color_temp")
if self.device["hiveType"] == "colourtuneablelight":
if self.device["status"]["mode"] == "COLOUR":
rgb = self.device["status"]["hs_color"]
self._attr_hs_color = color_util.color_RGB_to_hs(*rgb)
self._attr_color_mode = ColorMode.HS
else:
self._attr_color_temp = self.device["status"].get("color_temp")
self._attr_color_mode = ColorMode.COLOR_TEMP

View File

@ -16,7 +16,7 @@ from homeassistant.components.automation import (
)
from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA
from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback
from homeassistant.helpers.typing import ConfigType
from .const import DOMAIN, KNOWN_DEVICES, TRIGGERS
@ -86,13 +86,13 @@ class TriggerSource:
) -> CALLBACK_TYPE:
"""Attach a trigger."""
trigger_data = automation_info["trigger_data"]
job = HassJob(action)
@callback
def event_handler(char):
if config[CONF_SUBTYPE] != HK_TO_HA_INPUT_EVENT_VALUES[char["value"]]:
return
self._hass.async_create_task(
action({"trigger": {**trigger_data, **config}})
)
self._hass.async_run_hass_job(job, {"trigger": {**trigger_data, **config}})
trigger = self._triggers[config[CONF_TYPE], config[CONF_SUBTYPE]]
iid = trigger["characteristic"]
@ -231,11 +231,11 @@ async def async_setup_triggers_for_entry(hass: HomeAssistant, config_entry):
def async_fire_triggers(conn: HKDevice, events: dict[tuple[int, int], Any]):
"""Process events generated by a HomeKit accessory into automation triggers."""
trigger_sources: dict[str, TriggerSource] = conn.hass.data[TRIGGERS]
for (aid, iid), ev in events.items():
if aid in conn.devices:
device_id = conn.devices[aid]
if device_id in conn.hass.data[TRIGGERS]:
source = conn.hass.data[TRIGGERS][device_id]
if source := trigger_sources.get(device_id):
source.fire(iid, ev)

View File

@ -99,7 +99,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
client = Client(
host=host,
port=port,
loop=hass.loop,
update_interval=scan_interval.total_seconds(),
infer_arming_state=infer_arming_state,
)

View File

@ -2,7 +2,7 @@
"domain": "ness_alarm",
"name": "Ness Alarm",
"documentation": "https://www.home-assistant.io/integrations/ness_alarm",
"requirements": ["nessclient==0.9.15"],
"requirements": ["nessclient==0.10.0"],
"codeowners": ["@nickw444"],
"iot_class": "local_push",
"loggers": ["nessclient"]

View File

@ -59,7 +59,9 @@ class NetgearUpdateEntity(NetgearRouterEntity, UpdateEntity):
"""Latest version available for install."""
if self.coordinator.data is not None:
new_version = self.coordinator.data.get("NewVersion")
if new_version is not None:
if new_version is not None and not new_version.startswith(
self.installed_version
):
return new_version
return self.installed_version

View File

@ -416,7 +416,7 @@ class OpenThermGatewayDevice:
self.status = {}
self.update_signal = f"{DATA_OPENTHERM_GW}_{self.gw_id}_update"
self.options_update_signal = f"{DATA_OPENTHERM_GW}_{self.gw_id}_options_update"
self.gateway = pyotgw.pyotgw()
self.gateway = pyotgw.OpenThermGateway()
self.gw_version = None
async def cleanup(self, event=None):
@ -427,7 +427,7 @@ class OpenThermGatewayDevice:
async def connect_and_subscribe(self):
"""Connect to serial device and subscribe report handler."""
self.status = await self.gateway.connect(self.hass.loop, self.device_path)
self.status = await self.gateway.connect(self.device_path)
version_string = self.status[gw_vars.OTGW].get(gw_vars.OTGW_ABOUT)
self.gw_version = version_string[18:] if version_string else None
_LOGGER.debug(

View File

@ -59,8 +59,8 @@ class OpenThermGwConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
async def test_connection():
"""Try to connect to the OpenTherm Gateway."""
otgw = pyotgw.pyotgw()
status = await otgw.connect(self.hass.loop, device)
otgw = pyotgw.OpenThermGateway()
status = await otgw.connect(device)
await otgw.disconnect()
return status[gw_vars.OTGW].get(gw_vars.OTGW_ABOUT)

View File

@ -2,7 +2,7 @@
"domain": "opentherm_gw",
"name": "OpenTherm Gateway",
"documentation": "https://www.home-assistant.io/integrations/opentherm_gw",
"requirements": ["pyotgw==1.1b1"],
"requirements": ["pyotgw==2.0.0"],
"codeowners": ["@mvn23"],
"config_flow": true,
"iot_class": "local_push",

View File

@ -3,7 +3,7 @@
"name": "Shelly",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/shelly",
"requirements": ["aioshelly==2.0.0"],
"requirements": ["aioshelly==2.0.1"],
"zeroconf": [
{
"type": "_http._tcp.local.",

View File

@ -3,7 +3,7 @@
"name": "SimpliSafe",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/simplisafe",
"requirements": ["simplisafe-python==2022.06.1"],
"requirements": ["simplisafe-python==2022.07.0"],
"codeowners": ["@bachya"],
"iot_class": "cloud_polling",
"dhcp": [

View File

@ -187,14 +187,15 @@ def filter_libav_logging() -> None:
return logging.getLogger(__name__).isEnabledFor(logging.DEBUG)
for logging_namespace in (
"libav.mp4",
"libav.NULL",
"libav.h264",
"libav.hevc",
"libav.hls",
"libav.mp4",
"libav.mpegts",
"libav.rtsp",
"libav.tcp",
"libav.tls",
"libav.mpegts",
"libav.NULL",
):
logging.getLogger(logging_namespace).addFilter(libav_filter)

View File

@ -2,7 +2,7 @@
"domain": "switchbot",
"name": "SwitchBot",
"documentation": "https://www.home-assistant.io/integrations/switchbot",
"requirements": ["PySwitchbot==0.14.0"],
"requirements": ["PySwitchbot==0.14.1"],
"config_flow": true,
"codeowners": ["@danielhiversen", "@RenierM26"],
"iot_class": "local_polling",

View File

@ -3,7 +3,7 @@
"name": "Tomorrow.io",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/tomorrowio",
"requirements": ["pytomorrowio==0.3.3"],
"requirements": ["pytomorrowio==0.3.4"],
"codeowners": ["@raman325", "@lymanepp"],
"iot_class": "cloud_polling"
}

View File

@ -72,6 +72,7 @@ class ProtectData:
self._pending_camera_ids: set[str] = set()
self._unsub_interval: CALLBACK_TYPE | None = None
self._unsub_websocket: CALLBACK_TYPE | None = None
self._auth_failures = 0
self.last_update_success = False
self.api = protect
@ -117,6 +118,10 @@ class ProtectData:
try:
updates = await self.api.update(force=force)
except NotAuthorized:
if self._auth_failures < 10:
_LOGGER.exception("Auth error while updating")
self._auth_failures += 1
else:
await self.async_stop()
_LOGGER.exception("Reauthentication required")
self._entry.async_start_reauth(self._hass)
@ -129,6 +134,7 @@ class ProtectData:
self._async_process_updates(self.api.bootstrap)
else:
self.last_update_success = True
self._auth_failures = 0
self._async_process_updates(updates)
@callback

View File

@ -3,7 +3,7 @@
"name": "Venstar",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/venstar",
"requirements": ["venstarcolortouch==0.17"],
"requirements": ["venstarcolortouch==0.18"],
"codeowners": ["@garbled1"],
"iot_class": "local_polling",
"loggers": ["venstarcolortouch"]

View File

@ -73,7 +73,6 @@ CAPABILITIES_COLOR_LOOP = 0x4
CAPABILITIES_COLOR_XY = 0x08
CAPABILITIES_COLOR_TEMP = 0x10
DEFAULT_TRANSITION = 1
DEFAULT_MIN_BRIGHTNESS = 2
UPDATE_COLORLOOP_ACTION = 0x1
@ -119,7 +118,7 @@ class BaseLight(LogMixin, light.LightEntity):
"""Operations common to all light entities."""
_FORCE_ON = False
_DEFAULT_COLOR_FROM_OFF_TRANSITION = 0
_DEFAULT_MIN_TRANSITION_TIME = 0
def __init__(self, *args, **kwargs):
"""Initialize the light."""
@ -140,7 +139,7 @@ class BaseLight(LogMixin, light.LightEntity):
self._level_channel = None
self._color_channel = None
self._identify_channel = None
self._default_transition = None
self._zha_config_transition = self._DEFAULT_MIN_TRANSITION_TIME
self._attr_color_mode = ColorMode.UNKNOWN # Set by sub classes
@property
@ -216,33 +215,49 @@ class BaseLight(LogMixin, light.LightEntity):
transition = kwargs.get(light.ATTR_TRANSITION)
duration = (
transition * 10
if transition
else self._default_transition * 10
if self._default_transition
else DEFAULT_TRANSITION
)
if transition is not None
else self._zha_config_transition * 10
) or self._DEFAULT_MIN_TRANSITION_TIME # if 0 is passed in some devices still need the minimum default
brightness = kwargs.get(light.ATTR_BRIGHTNESS)
effect = kwargs.get(light.ATTR_EFFECT)
flash = kwargs.get(light.ATTR_FLASH)
temperature = kwargs.get(light.ATTR_COLOR_TEMP)
hs_color = kwargs.get(light.ATTR_HS_COLOR)
# If the light is currently off but a turn_on call with a color/temperature is sent,
# the light needs to be turned on first at a low brightness level where the light is immediately transitioned
# to the correct color. Afterwards, the transition is only from the low brightness to the new brightness.
# Otherwise, the transition is from the color the light had before being turned on to the new color.
# This can look especially bad with transitions longer than a second.
color_provided_from_off = (
not self._state
and brightness_supported(self._attr_supported_color_modes)
and (light.ATTR_COLOR_TEMP in kwargs or light.ATTR_HS_COLOR in kwargs)
# This can look especially bad with transitions longer than a second. We do not want to do this for
# devices that need to be forced to use the on command because we would end up with 4 commands sent:
# move to level, on, color, move to level... We also will not set this if the bulb is already in the
# desired color mode with the desired color or color temperature.
new_color_provided_while_off = (
not isinstance(self, LightGroup)
and not self._FORCE_ON
and not self._state
and (
(
temperature is not None
and (
self._color_temp != temperature
or self._attr_color_mode != ColorMode.COLOR_TEMP
)
)
or (
hs_color is not None
and (
self.hs_color != hs_color
or self._attr_color_mode != ColorMode.HS
)
)
)
and brightness_supported(self._attr_supported_color_modes)
)
final_duration = duration
if color_provided_from_off:
# Set the duration for the color changing commands to 0.
duration = 0
if (
brightness is None
and (self._off_with_transition or color_provided_from_off)
and (self._off_with_transition or new_color_provided_while_off)
and self._off_brightness is not None
):
brightness = self._off_brightness
@ -254,11 +269,11 @@ class BaseLight(LogMixin, light.LightEntity):
t_log = {}
if color_provided_from_off:
if new_color_provided_while_off:
# If the light is currently off, we first need to turn it on at a low brightness level with no transition.
# After that, we set it to the desired color/temperature with no transition.
result = await self._level_channel.move_to_level_with_on_off(
DEFAULT_MIN_BRIGHTNESS, self._DEFAULT_COLOR_FROM_OFF_TRANSITION
DEFAULT_MIN_BRIGHTNESS, self._DEFAULT_MIN_TRANSITION_TIME
)
t_log["move_to_level_with_on_off"] = result
if isinstance(result, Exception) or result[1] is not Status.SUCCESS:
@ -269,7 +284,7 @@ class BaseLight(LogMixin, light.LightEntity):
if (
(brightness is not None or transition)
and not color_provided_from_off
and not new_color_provided_while_off
and brightness_supported(self._attr_supported_color_modes)
):
result = await self._level_channel.move_to_level_with_on_off(
@ -285,7 +300,7 @@ class BaseLight(LogMixin, light.LightEntity):
if (
brightness is None
and not color_provided_from_off
and not new_color_provided_while_off
or (self._FORCE_ON and brightness)
):
# since some lights don't always turn on with move_to_level_with_on_off,
@ -297,9 +312,13 @@ class BaseLight(LogMixin, light.LightEntity):
return
self._state = True
if light.ATTR_COLOR_TEMP in kwargs:
temperature = kwargs[light.ATTR_COLOR_TEMP]
result = await self._color_channel.move_to_color_temp(temperature, duration)
if temperature is not None:
result = await self._color_channel.move_to_color_temp(
temperature,
self._DEFAULT_MIN_TRANSITION_TIME
if new_color_provided_while_off
else duration,
)
t_log["move_to_color_temp"] = result
if isinstance(result, Exception) or result[1] is not Status.SUCCESS:
self.debug("turned on: %s", t_log)
@ -308,11 +327,14 @@ class BaseLight(LogMixin, light.LightEntity):
self._color_temp = temperature
self._hs_color = None
if light.ATTR_HS_COLOR in kwargs:
hs_color = kwargs[light.ATTR_HS_COLOR]
if hs_color is not None:
xy_color = color_util.color_hs_to_xy(*hs_color)
result = await self._color_channel.move_to_color(
int(xy_color[0] * 65535), int(xy_color[1] * 65535), duration
int(xy_color[0] * 65535),
int(xy_color[1] * 65535),
self._DEFAULT_MIN_TRANSITION_TIME
if new_color_provided_while_off
else duration,
)
t_log["move_to_color"] = result
if isinstance(result, Exception) or result[1] is not Status.SUCCESS:
@ -322,9 +344,9 @@ class BaseLight(LogMixin, light.LightEntity):
self._hs_color = hs_color
self._color_temp = None
if color_provided_from_off:
if new_color_provided_while_off:
# The light is has the correct color, so we can now transition it to the correct brightness level.
result = await self._level_channel.move_to_level(level, final_duration)
result = await self._level_channel.move_to_level(level, duration)
t_log["move_to_level_if_color"] = result
if isinstance(result, Exception) or result[1] is not Status.SUCCESS:
self.debug("turned on: %s", t_log)
@ -371,12 +393,13 @@ class BaseLight(LogMixin, light.LightEntity):
async def async_turn_off(self, **kwargs):
"""Turn the entity off."""
duration = kwargs.get(light.ATTR_TRANSITION)
transition = kwargs.get(light.ATTR_TRANSITION)
supports_level = brightness_supported(self._attr_supported_color_modes)
if duration and supports_level:
# is not none looks odd here but it will override built in bulb transition times if we pass 0 in here
if transition is not None and supports_level:
result = await self._level_channel.move_to_level_with_on_off(
0, duration * 10
0, transition * 10
)
else:
result = await self._on_off_channel.off()
@ -387,7 +410,7 @@ class BaseLight(LogMixin, light.LightEntity):
if supports_level:
# store current brightness so that the next turn_on uses it.
self._off_with_transition = bool(duration)
self._off_with_transition = transition is not None
self._off_brightness = self._brightness
self.async_write_ha_state()
@ -460,7 +483,7 @@ class Light(BaseLight, ZhaEntity):
if effect_list:
self._effect_list = effect_list
self._default_transition = async_get_zha_config_value(
self._zha_config_transition = async_get_zha_config_value(
zha_device.gateway.config_entry,
ZHA_OPTIONS,
CONF_DEFAULT_LIGHT_TRANSITION,
@ -472,6 +495,7 @@ class Light(BaseLight, ZhaEntity):
"""Set the state."""
self._state = bool(value)
if value:
self._off_with_transition = False
self._off_brightness = None
self.async_write_ha_state()
@ -605,7 +629,7 @@ class HueLight(Light):
@STRICT_MATCH(
channel_names=CHANNEL_ON_OFF,
aux_channels={CHANNEL_COLOR, CHANNEL_LEVEL},
manufacturers={"Jasco", "Quotra-Vision"},
manufacturers={"Jasco", "Quotra-Vision", "eWeLight", "eWeLink"},
)
class ForceOnLight(Light):
"""Representation of a light which does not respect move_to_level_with_on_off."""
@ -621,7 +645,7 @@ class ForceOnLight(Light):
class SengledLight(Light):
"""Representation of a Sengled light which does not react to move_to_color_temp with 0 as a transition."""
_DEFAULT_COLOR_FROM_OFF_TRANSITION = 1
_DEFAULT_MIN_TRANSITION_TIME = 1
@GROUP_MATCH()
@ -639,7 +663,7 @@ class LightGroup(BaseLight, ZhaGroupEntity):
self._color_channel = group.endpoint[Color.cluster_id]
self._identify_channel = group.endpoint[Identify.cluster_id]
self._debounced_member_refresh = None
self._default_transition = async_get_zha_config_value(
self._zha_config_transition = async_get_zha_config_value(
zha_device.gateway.config_entry,
ZHA_OPTIONS,
CONF_DEFAULT_LIGHT_TRANSITION,

View File

@ -7,7 +7,7 @@ from .backports.enum import StrEnum
MAJOR_VERSION: Final = 2022
MINOR_VERSION: Final = 7
PATCH_VERSION: Final = "5"
PATCH_VERSION: Final = "6"
__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

@ -11,6 +11,10 @@ import orjson
from homeassistant.core import Event, State
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.json import (
JSONEncoder as DefaultHASSJSONEncoder,
json_encoder_default as default_hass_orjson_encoder,
)
from .file import write_utf8_file, write_utf8_file_atomic
@ -45,10 +49,12 @@ def load_json(filename: str, default: list | dict | None = None) -> list | dict:
return {} if default is None else default
def _orjson_encoder(data: Any) -> str:
"""JSON encoder that uses orjson."""
def _orjson_default_encoder(data: Any) -> str:
"""JSON encoder that uses orjson with hass defaults."""
return orjson.dumps(
data, option=orjson.OPT_INDENT_2 | orjson.OPT_NON_STR_KEYS
data,
option=orjson.OPT_INDENT_2 | orjson.OPT_NON_STR_KEYS,
default=default_hass_orjson_encoder,
).decode("utf-8")
@ -64,13 +70,19 @@ def save_json(
Returns True on success.
"""
dump: Callable[[Any], Any] = json.dumps
dump: Callable[[Any], Any]
try:
if encoder:
# For backwards compatibility, if they pass in the
# default json encoder we use _orjson_default_encoder
# which is the orjson equivalent to the default encoder.
if encoder and encoder is not DefaultHASSJSONEncoder:
# If they pass a custom encoder that is not the
# DefaultHASSJSONEncoder, we use the slow path of json.dumps
dump = json.dumps
json_data = json.dumps(data, indent=2, cls=encoder)
else:
dump = _orjson_encoder
json_data = _orjson_encoder(data)
dump = _orjson_default_encoder
json_data = _orjson_default_encoder(data)
except TypeError as error:
msg = f"Failed to serialize to JSON: {filename}. Bad data at {format_unserializable_data(find_paths_unserializable_data(data, dump=dump))}"
_LOGGER.error(msg)

View File

@ -4,7 +4,7 @@ from __future__ import annotations
from collections import OrderedDict
from collections.abc import Iterator
import fnmatch
from io import StringIO
from io import StringIO, TextIOWrapper
import logging
import os
from pathlib import Path
@ -169,7 +169,7 @@ def parse_yaml(
except yaml.YAMLError:
# Loading failed, so we now load with the slow line loader
# since the C one will not give us line numbers
if isinstance(content, (StringIO, TextIO)):
if isinstance(content, (StringIO, TextIO, TextIOWrapper)):
# Rewind the stream so we can try again
content.seek(0, 0)
return _parse_yaml_pure_python(content, secrets)

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "homeassistant"
version = "2022.7.5"
version = "2022.7.6"
license = {text = "Apache-2.0"}
description = "Open-source home automation platform running on Python 3."
readme = "README.rst"

View File

@ -5,7 +5,7 @@
AEMET-OpenData==0.2.1
# homeassistant.components.aladdin_connect
AIOAladdinConnect==0.1.25
AIOAladdinConnect==0.1.27
# homeassistant.components.adax
Adax-local==0.1.4
@ -37,7 +37,7 @@ PyRMVtransport==0.3.3
PySocks==1.7.1
# homeassistant.components.switchbot
PySwitchbot==0.14.0
PySwitchbot==0.14.1
# homeassistant.components.transport_nsw
PyTransportNSW==0.1.1
@ -244,7 +244,7 @@ aiosenseme==0.6.1
aiosenz==1.0.0
# homeassistant.components.shelly
aioshelly==2.0.0
aioshelly==2.0.1
# homeassistant.components.skybell
aioskybell==22.7.0
@ -396,7 +396,7 @@ beautifulsoup4==4.11.1
bellows==0.31.1
# homeassistant.components.bmw_connected_drive
bimmer_connected==0.9.6
bimmer_connected==0.10.1
# homeassistant.components.bizkaibus
bizkaibus==0.1.1
@ -1068,7 +1068,7 @@ nad_receiver==0.3.0
ndms2_client==0.1.1
# homeassistant.components.ness_alarm
nessclient==0.9.15
nessclient==0.10.0
# homeassistant.components.netdata
netdata==1.0.1
@ -1366,7 +1366,7 @@ pyaftership==21.11.0
pyairnow==1.1.0
# homeassistant.components.airvisual
pyairvisual==5.0.9
pyairvisual==2022.07.0
# homeassistant.components.almond
pyalmond==0.0.2
@ -1715,7 +1715,7 @@ pyopnsense==0.2.0
pyoppleio==1.0.5
# homeassistant.components.opentherm_gw
pyotgw==1.1b1
pyotgw==2.0.0
# homeassistant.auth.mfa_modules.notify
# homeassistant.auth.mfa_modules.totp
@ -1973,7 +1973,7 @@ pythonegardia==1.0.40
pytile==2022.02.0
# homeassistant.components.tomorrowio
pytomorrowio==0.3.3
pytomorrowio==0.3.4
# homeassistant.components.touchline
pytouchline==0.7
@ -2168,7 +2168,7 @@ simplehound==0.3
simplepush==1.1.4
# homeassistant.components.simplisafe
simplisafe-python==2022.06.1
simplisafe-python==2022.07.0
# homeassistant.components.sisyphus
sisyphus-control==3.1.2
@ -2387,7 +2387,7 @@ vehicle==0.4.0
velbus-aio==2022.6.2
# homeassistant.components.venstar
venstarcolortouch==0.17
venstarcolortouch==0.18
# homeassistant.components.vilfo
vilfo-api-client==0.3.2

View File

@ -7,7 +7,7 @@
AEMET-OpenData==0.2.1
# homeassistant.components.aladdin_connect
AIOAladdinConnect==0.1.25
AIOAladdinConnect==0.1.27
# homeassistant.components.adax
Adax-local==0.1.4
@ -33,7 +33,7 @@ PyRMVtransport==0.3.3
PySocks==1.7.1
# homeassistant.components.switchbot
PySwitchbot==0.14.0
PySwitchbot==0.14.1
# homeassistant.components.transport_nsw
PyTransportNSW==0.1.1
@ -213,7 +213,7 @@ aiosenseme==0.6.1
aiosenz==1.0.0
# homeassistant.components.shelly
aioshelly==2.0.0
aioshelly==2.0.1
# homeassistant.components.skybell
aioskybell==22.7.0
@ -311,7 +311,7 @@ beautifulsoup4==4.11.1
bellows==0.31.1
# homeassistant.components.bmw_connected_drive
bimmer_connected==0.9.6
bimmer_connected==0.10.1
# homeassistant.components.blebox
blebox_uniapi==2.0.1
@ -742,7 +742,7 @@ mutesync==0.0.1
ndms2_client==0.1.1
# homeassistant.components.ness_alarm
nessclient==0.9.15
nessclient==0.10.0
# homeassistant.components.discovery
netdisco==3.0.0
@ -929,7 +929,7 @@ pyaehw4a1==0.3.9
pyairnow==1.1.0
# homeassistant.components.airvisual
pyairvisual==5.0.9
pyairvisual==2022.07.0
# homeassistant.components.almond
pyalmond==0.0.2
@ -1167,7 +1167,7 @@ pyopenuv==2022.04.0
pyopnsense==0.2.0
# homeassistant.components.opentherm_gw
pyotgw==1.1b1
pyotgw==2.0.0
# homeassistant.auth.mfa_modules.notify
# homeassistant.auth.mfa_modules.totp
@ -1314,7 +1314,7 @@ python_awair==0.2.3
pytile==2022.02.0
# homeassistant.components.tomorrowio
pytomorrowio==0.3.3
pytomorrowio==0.3.4
# homeassistant.components.traccar
pytraccar==0.10.0
@ -1440,7 +1440,7 @@ simplehound==0.3
simplepush==1.1.4
# homeassistant.components.simplisafe
simplisafe-python==2022.06.1
simplisafe-python==2022.07.0
# homeassistant.components.slack
slackclient==2.5.0
@ -1590,7 +1590,7 @@ vehicle==0.4.0
velbus-aio==2022.6.2
# homeassistant.components.venstar
venstarcolortouch==0.17
venstarcolortouch==0.18
# homeassistant.components.vilfo
vilfo-api-client==0.3.2

View File

@ -4,8 +4,10 @@ from unittest.mock import patch
from pyairvisual.errors import (
AirVisualError,
InvalidKeyError,
KeyExpiredError,
NodeProError,
NotFoundError,
UnauthorizedError,
)
import pytest
@ -84,6 +86,28 @@ async def test_duplicate_error(hass, config, config_entry, data):
{CONF_API_KEY: "invalid_api_key"},
INTEGRATION_TYPE_GEOGRAPHY_NAME,
),
(
{
CONF_API_KEY: "abcde12345",
CONF_CITY: "Beijing",
CONF_STATE: "Beijing",
CONF_COUNTRY: "China",
},
KeyExpiredError,
{CONF_API_KEY: "invalid_api_key"},
INTEGRATION_TYPE_GEOGRAPHY_NAME,
),
(
{
CONF_API_KEY: "abcde12345",
CONF_CITY: "Beijing",
CONF_STATE: "Beijing",
CONF_COUNTRY: "China",
},
UnauthorizedError,
{CONF_API_KEY: "invalid_api_key"},
INTEGRATION_TYPE_GEOGRAPHY_NAME,
),
(
{
CONF_API_KEY: "abcde12345",

View File

@ -10,6 +10,7 @@ DEVICE_CONFIG_OPEN = {
"name": "home",
"status": "open",
"link_status": "Connected",
"serial": "12345",
}

View File

@ -33,6 +33,7 @@ DEVICE_CONFIG_OPEN = {
"name": "home",
"status": "open",
"link_status": "Connected",
"serial": "12345",
}
DEVICE_CONFIG_OPENING = {
@ -41,6 +42,7 @@ DEVICE_CONFIG_OPENING = {
"name": "home",
"status": "opening",
"link_status": "Connected",
"serial": "12345",
}
DEVICE_CONFIG_CLOSED = {
@ -49,6 +51,7 @@ DEVICE_CONFIG_CLOSED = {
"name": "home",
"status": "closed",
"link_status": "Connected",
"serial": "12345",
}
DEVICE_CONFIG_CLOSING = {
@ -57,6 +60,7 @@ DEVICE_CONFIG_CLOSING = {
"name": "home",
"status": "closing",
"link_status": "Connected",
"serial": "12345",
}
DEVICE_CONFIG_DISCONNECTED = {
@ -65,6 +69,7 @@ DEVICE_CONFIG_DISCONNECTED = {
"name": "home",
"status": "open",
"link_status": "Disconnected",
"serial": "12345",
}
DEVICE_CONFIG_BAD = {

View File

@ -43,10 +43,12 @@ async def test_form_user(hass):
"homeassistant.components.opentherm_gw.async_setup_entry",
return_value=True,
) as mock_setup_entry, patch(
"pyotgw.pyotgw.connect", return_value=MINIMAL_STATUS
"pyotgw.OpenThermGateway.connect", return_value=MINIMAL_STATUS
) as mock_pyotgw_connect, patch(
"pyotgw.pyotgw.disconnect", return_value=None
) as mock_pyotgw_disconnect:
"pyotgw.OpenThermGateway.disconnect", return_value=None
) as mock_pyotgw_disconnect, patch(
"pyotgw.status.StatusManager._process_updates", return_value=None
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"], {CONF_NAME: "Test Entry 1", CONF_DEVICE: "/dev/ttyUSB0"}
)
@ -75,10 +77,12 @@ async def test_form_import(hass):
"homeassistant.components.opentherm_gw.async_setup_entry",
return_value=True,
) as mock_setup_entry, patch(
"pyotgw.pyotgw.connect", return_value=MINIMAL_STATUS
"pyotgw.OpenThermGateway.connect", return_value=MINIMAL_STATUS
) as mock_pyotgw_connect, patch(
"pyotgw.pyotgw.disconnect", return_value=None
) as mock_pyotgw_disconnect:
"pyotgw.OpenThermGateway.disconnect", return_value=None
) as mock_pyotgw_disconnect, patch(
"pyotgw.status.StatusManager._process_updates", return_value=None
):
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_IMPORT},
@ -117,10 +121,12 @@ async def test_form_duplicate_entries(hass):
"homeassistant.components.opentherm_gw.async_setup_entry",
return_value=True,
) as mock_setup_entry, patch(
"pyotgw.pyotgw.connect", return_value=MINIMAL_STATUS
"pyotgw.OpenThermGateway.connect", return_value=MINIMAL_STATUS
) as mock_pyotgw_connect, patch(
"pyotgw.pyotgw.disconnect", return_value=None
) as mock_pyotgw_disconnect:
"pyotgw.OpenThermGateway.disconnect", return_value=None
) as mock_pyotgw_disconnect, patch(
"pyotgw.status.StatusManager._process_updates", return_value=None
):
result1 = await hass.config_entries.flow.async_configure(
flow1["flow_id"], {CONF_NAME: "Test Entry 1", CONF_DEVICE: "/dev/ttyUSB0"}
)
@ -148,8 +154,10 @@ async def test_form_connection_timeout(hass):
)
with patch(
"pyotgw.pyotgw.connect", side_effect=(asyncio.TimeoutError)
) as mock_connect:
"pyotgw.OpenThermGateway.connect", side_effect=(asyncio.TimeoutError)
) as mock_connect, patch(
"pyotgw.status.StatusManager._process_updates", return_value=None
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_NAME: "Test Entry 1", CONF_DEVICE: "socket://192.0.2.254:1234"},
@ -166,7 +174,11 @@ async def test_form_connection_error(hass):
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
with patch("pyotgw.pyotgw.connect", side_effect=(SerialException)) as mock_connect:
with patch(
"pyotgw.OpenThermGateway.connect", side_effect=(SerialException)
) as mock_connect, patch(
"pyotgw.status.StatusManager._process_updates", return_value=None
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"], {CONF_NAME: "Test Entry 1", CONF_DEVICE: "/dev/ttyUSB0"}
)
@ -196,7 +208,11 @@ async def test_options_migration(hass):
with patch(
"homeassistant.components.opentherm_gw.OpenThermGatewayDevice.connect_and_subscribe",
return_value=True,
), patch("homeassistant.components.opentherm_gw.async_setup", return_value=True):
), patch(
"homeassistant.components.opentherm_gw.async_setup", return_value=True
), patch(
"pyotgw.status.StatusManager._process_updates", return_value=None
):
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()

View File

@ -34,7 +34,7 @@ async def test_device_registry_insert(hass):
with patch(
"homeassistant.components.opentherm_gw.OpenThermGatewayDevice.cleanup",
return_value=None,
), patch("pyotgw.pyotgw.connect", return_value=MINIMAL_STATUS):
), patch("pyotgw.OpenThermGateway.connect", return_value=MINIMAL_STATUS):
await setup.async_setup_component(hass, DOMAIN, {})
await hass.async_block_till_done()
@ -62,7 +62,7 @@ async def test_device_registry_update(hass):
with patch(
"homeassistant.components.opentherm_gw.OpenThermGatewayDevice.cleanup",
return_value=None,
), patch("pyotgw.pyotgw.connect", return_value=MINIMAL_STATUS_UPD):
), patch("pyotgw.OpenThermGateway.connect", return_value=MINIMAL_STATUS_UPD):
await setup.async_setup_component(hass, DOMAIN, {})
await hass.async_block_till_done()

View File

@ -9,14 +9,18 @@ import aiohttp
from pyunifiprotect import NotAuthorized, NvrError, ProtectApiClient
from pyunifiprotect.data import NVR, Bootstrap, Light
from homeassistant.components.unifiprotect.const import CONF_DISABLE_RTSP, DOMAIN
from homeassistant.components.unifiprotect.const import (
CONF_DISABLE_RTSP,
DEFAULT_SCAN_INTERVAL,
DOMAIN,
)
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.setup import async_setup_component
from . import _patch_discovery
from .utils import MockUFPFixture, init_entry
from .utils import MockUFPFixture, init_entry, time_changed
from tests.common import MockConfigEntry
@ -145,12 +149,23 @@ async def test_setup_failed_update(hass: HomeAssistant, ufp: MockUFPFixture):
async def test_setup_failed_update_reauth(hass: HomeAssistant, ufp: MockUFPFixture):
"""Test setup of unifiprotect entry with update that gives unauthroized error."""
ufp.api.update = AsyncMock(side_effect=NotAuthorized)
await hass.config_entries.async_setup(ufp.entry.entry_id)
await hass.async_block_till_done()
assert ufp.entry.state == ConfigEntryState.SETUP_RETRY
assert ufp.api.update.called
assert ufp.entry.state == ConfigEntryState.LOADED
# reauth should not be triggered until there are 10 auth failures in a row
# to verify it is not transient
ufp.api.update = AsyncMock(side_effect=NotAuthorized)
for _ in range(10):
await time_changed(hass, DEFAULT_SCAN_INTERVAL)
assert len(hass.config_entries.flow._progress) == 0
assert ufp.api.update.call_count == 10
assert ufp.entry.state == ConfigEntryState.LOADED
await time_changed(hass, DEFAULT_SCAN_INTERVAL)
assert ufp.api.update.call_count == 11
assert len(hass.config_entries.flow._progress) == 1
async def test_setup_failed_error(hass: HomeAssistant, ufp: MockUFPFixture):

View File

@ -15,7 +15,11 @@ from homeassistant.components.light import (
ColorMode,
)
from homeassistant.components.zha.core.group import GroupMember
from homeassistant.components.zha.light import FLASH_EFFECTS
from homeassistant.components.zha.light import (
CAPABILITIES_COLOR_TEMP,
CAPABILITIES_COLOR_XY,
FLASH_EFFECTS,
)
from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE, Platform
import homeassistant.util.dt as dt_util
@ -142,6 +146,10 @@ async def device_light_1(hass, zigpy_device_mock, zha_device_joined):
ieee=IEEE_GROUPABLE_DEVICE,
nwk=0xB79D,
)
color_cluster = zigpy_device.endpoints[1].light_color
color_cluster.PLUGGED_ATTR_READS = {
"color_capabilities": CAPABILITIES_COLOR_TEMP | CAPABILITIES_COLOR_XY
}
zha_device = await zha_device_joined(zigpy_device)
zha_device.available = True
return zha_device
@ -167,8 +175,13 @@ async def device_light_2(hass, zigpy_device_mock, zha_device_joined):
}
},
ieee=IEEE_GROUPABLE_DEVICE2,
manufacturer="Sengled",
nwk=0xC79E,
)
color_cluster = zigpy_device.endpoints[1].light_color
color_cluster.PLUGGED_ATTR_READS = {
"color_capabilities": CAPABILITIES_COLOR_TEMP | CAPABILITIES_COLOR_XY
}
zha_device = await zha_device_joined(zigpy_device)
zha_device.available = True
return zha_device
@ -201,6 +214,38 @@ async def device_light_3(hass, zigpy_device_mock, zha_device_joined):
return zha_device
@pytest.fixture
async def eWeLink_light(hass, zigpy_device_mock, zha_device_joined):
"""Mock eWeLink light."""
zigpy_device = zigpy_device_mock(
{
1: {
SIG_EP_INPUT: [
general.OnOff.cluster_id,
general.LevelControl.cluster_id,
lighting.Color.cluster_id,
general.Groups.cluster_id,
general.Identify.cluster_id,
],
SIG_EP_OUTPUT: [],
SIG_EP_TYPE: zha.DeviceType.COLOR_DIMMABLE_LIGHT,
SIG_EP_PROFILE: zha.PROFILE_ID,
}
},
ieee="03:2d:6f:00:0a:90:69:e3",
manufacturer="eWeLink",
nwk=0xB79D,
)
color_cluster = zigpy_device.endpoints[1].light_color
color_cluster.PLUGGED_ATTR_READS = {
"color_capabilities": CAPABILITIES_COLOR_TEMP | CAPABILITIES_COLOR_XY
}
zha_device = await zha_device_joined(zigpy_device)
zha_device.available = True
return zha_device
async def test_light_refresh(hass, zigpy_device_mock, zha_device_joined_restored):
"""Test zha light platform refresh."""
@ -323,6 +368,758 @@ async def test_light(
await async_test_flash_from_hass(hass, cluster_identify, entity_id, FLASH_LONG)
@patch(
"zigpy.zcl.clusters.lighting.Color.request",
new=AsyncMock(return_value=[sentinel.data, zcl_f.Status.SUCCESS]),
)
@patch(
"zigpy.zcl.clusters.general.Identify.request",
new=AsyncMock(return_value=[sentinel.data, zcl_f.Status.SUCCESS]),
)
@patch(
"zigpy.zcl.clusters.general.LevelControl.request",
new=AsyncMock(return_value=[sentinel.data, zcl_f.Status.SUCCESS]),
)
@patch(
"zigpy.zcl.clusters.general.OnOff.request",
new=AsyncMock(return_value=[sentinel.data, zcl_f.Status.SUCCESS]),
)
async def test_transitions(
hass, device_light_1, device_light_2, eWeLink_light, coordinator
):
"""Test ZHA light transition code."""
zha_gateway = get_zha_gateway(hass)
assert zha_gateway is not None
zha_gateway.coordinator_zha_device = coordinator
coordinator._zha_gateway = zha_gateway
device_light_1._zha_gateway = zha_gateway
device_light_2._zha_gateway = zha_gateway
member_ieee_addresses = [device_light_1.ieee, device_light_2.ieee]
members = [GroupMember(device_light_1.ieee, 1), GroupMember(device_light_2.ieee, 1)]
assert coordinator.is_coordinator
# test creating a group with 2 members
zha_group = await zha_gateway.async_create_zigpy_group("Test Group", members)
await hass.async_block_till_done()
assert zha_group is not None
assert len(zha_group.members) == 2
for member in zha_group.members:
assert member.device.ieee in member_ieee_addresses
assert member.group == zha_group
assert member.endpoint is not None
device_1_entity_id = await find_entity_id(Platform.LIGHT, device_light_1, hass)
device_2_entity_id = await find_entity_id(Platform.LIGHT, device_light_2, hass)
eWeLink_light_entity_id = await find_entity_id(Platform.LIGHT, eWeLink_light, hass)
assert device_1_entity_id != device_2_entity_id
group_entity_id = async_find_group_entity_id(hass, Platform.LIGHT, zha_group)
assert hass.states.get(group_entity_id) is not None
assert device_1_entity_id in zha_group.member_entity_ids
assert device_2_entity_id in zha_group.member_entity_ids
dev1_cluster_on_off = device_light_1.device.endpoints[1].on_off
dev2_cluster_on_off = device_light_2.device.endpoints[1].on_off
eWeLink_cluster_on_off = eWeLink_light.device.endpoints[1].on_off
dev1_cluster_level = device_light_1.device.endpoints[1].level
dev2_cluster_level = device_light_2.device.endpoints[1].level
eWeLink_cluster_level = eWeLink_light.device.endpoints[1].level
dev1_cluster_color = device_light_1.device.endpoints[1].light_color
dev2_cluster_color = device_light_2.device.endpoints[1].light_color
eWeLink_cluster_color = eWeLink_light.device.endpoints[1].light_color
# allow traffic to flow through the gateway and device
await async_enable_traffic(hass, [device_light_1, device_light_2])
await async_wait_for_updates(hass)
# test that the lights were created and are off
group_state = hass.states.get(group_entity_id)
assert group_state.state == STATE_OFF
light1_state = hass.states.get(device_1_entity_id)
assert light1_state.state == STATE_OFF
light2_state = hass.states.get(device_2_entity_id)
assert light2_state.state == STATE_OFF
# first test 0 length transition with no color provided
dev1_cluster_on_off.request.reset_mock()
dev1_cluster_level.request.reset_mock()
await hass.services.async_call(
LIGHT_DOMAIN,
"turn_on",
{"entity_id": device_1_entity_id, "transition": 0, "brightness": 50},
blocking=True,
)
assert dev1_cluster_on_off.request.call_count == 0
assert dev1_cluster_on_off.request.await_count == 0
assert dev1_cluster_color.request.call_count == 0
assert dev1_cluster_color.request.await_count == 0
assert dev1_cluster_level.request.call_count == 1
assert dev1_cluster_level.request.await_count == 1
assert dev1_cluster_level.request.call_args == call(
False,
4,
dev1_cluster_level.commands_by_name["move_to_level_with_on_off"].schema,
50, # brightness (level in ZCL)
0, # transition time
expect_reply=True,
manufacturer=None,
tries=1,
tsn=None,
)
light1_state = hass.states.get(device_1_entity_id)
assert light1_state.state == STATE_ON
assert light1_state.attributes["brightness"] == 50
dev1_cluster_level.request.reset_mock()
# test non 0 length transition with color provided while light is on
await hass.services.async_call(
LIGHT_DOMAIN,
"turn_on",
{
"entity_id": device_1_entity_id,
"transition": 3,
"brightness": 18,
"color_temp": 432,
},
blocking=True,
)
assert dev1_cluster_on_off.request.call_count == 0
assert dev1_cluster_on_off.request.await_count == 0
assert dev1_cluster_color.request.call_count == 1
assert dev1_cluster_color.request.await_count == 1
assert dev1_cluster_level.request.call_count == 1
assert dev1_cluster_level.request.await_count == 1
assert dev1_cluster_level.request.call_args == call(
False,
4,
dev1_cluster_level.commands_by_name["move_to_level_with_on_off"].schema,
18, # brightness (level in ZCL)
30, # transition time (ZCL time in 10ths of a second)
expect_reply=True,
manufacturer=None,
tries=1,
tsn=None,
)
assert dev1_cluster_color.request.call_args == call(
False,
10,
dev1_cluster_color.commands_by_name["move_to_color_temp"].schema,
432, # color temp mireds
30.0, # transition time (ZCL time in 10ths of a second)
expect_reply=True,
manufacturer=None,
tries=1,
tsn=None,
)
light1_state = hass.states.get(device_1_entity_id)
assert light1_state.state == STATE_ON
assert light1_state.attributes["brightness"] == 18
assert light1_state.attributes["color_temp"] == 432
assert light1_state.attributes["color_mode"] == ColorMode.COLOR_TEMP
dev1_cluster_level.request.reset_mock()
dev1_cluster_color.request.reset_mock()
# test 0 length transition to turn light off
await hass.services.async_call(
LIGHT_DOMAIN,
"turn_off",
{
"entity_id": device_1_entity_id,
"transition": 0,
},
blocking=True,
)
assert dev1_cluster_on_off.request.call_count == 0
assert dev1_cluster_on_off.request.await_count == 0
assert dev1_cluster_color.request.call_count == 0
assert dev1_cluster_color.request.await_count == 0
assert dev1_cluster_level.request.call_count == 1
assert dev1_cluster_level.request.await_count == 1
assert dev1_cluster_level.request.call_args == call(
False,
4,
dev1_cluster_level.commands_by_name["move_to_level_with_on_off"].schema,
0, # brightness (level in ZCL)
0, # transition time (ZCL time in 10ths of a second)
expect_reply=True,
manufacturer=None,
tries=1,
tsn=None,
)
light1_state = hass.states.get(device_1_entity_id)
assert light1_state.state == STATE_OFF
dev1_cluster_level.request.reset_mock()
# test non 0 length transition and color temp while turning light on (color_provided_while_off)
await hass.services.async_call(
LIGHT_DOMAIN,
"turn_on",
{
"entity_id": device_1_entity_id,
"transition": 1,
"brightness": 25,
"color_temp": 235,
},
blocking=True,
)
assert dev1_cluster_on_off.request.call_count == 0
assert dev1_cluster_on_off.request.await_count == 0
assert dev1_cluster_color.request.call_count == 1
assert dev1_cluster_color.request.await_count == 1
assert dev1_cluster_level.request.call_count == 2
assert dev1_cluster_level.request.await_count == 2
# first it comes on with no transition at 2 brightness
assert dev1_cluster_level.request.call_args_list[0] == call(
False,
4,
dev1_cluster_level.commands_by_name["move_to_level_with_on_off"].schema,
2, # brightness (level in ZCL)
0, # transition time (ZCL time in 10ths of a second)
expect_reply=True,
manufacturer=None,
tries=1,
tsn=None,
)
assert dev1_cluster_color.request.call_args == call(
False,
10,
dev1_cluster_color.commands_by_name["move_to_color_temp"].schema,
235, # color temp mireds
0, # transition time (ZCL time in 10ths of a second) - no transition when color_provided_while_off
expect_reply=True,
manufacturer=None,
tries=1,
tsn=None,
)
assert dev1_cluster_level.request.call_args_list[1] == call(
False,
0,
dev1_cluster_level.commands_by_name["move_to_level"].schema,
25, # brightness (level in ZCL)
10.0, # transition time (ZCL time in 10ths of a second)
expect_reply=True,
manufacturer=None,
tries=1,
tsn=None,
)
light1_state = hass.states.get(device_1_entity_id)
assert light1_state.state == STATE_ON
assert light1_state.attributes["brightness"] == 25
assert light1_state.attributes["color_temp"] == 235
assert light1_state.attributes["color_mode"] == ColorMode.COLOR_TEMP
dev1_cluster_level.request.reset_mock()
dev1_cluster_color.request.reset_mock()
# turn light 1 back off
await hass.services.async_call(
LIGHT_DOMAIN,
"turn_off",
{
"entity_id": device_1_entity_id,
},
blocking=True,
)
assert dev1_cluster_on_off.request.call_count == 1
assert dev1_cluster_on_off.request.await_count == 1
assert dev1_cluster_color.request.call_count == 0
assert dev1_cluster_color.request.await_count == 0
assert dev1_cluster_level.request.call_count == 0
assert dev1_cluster_level.request.await_count == 0
group_state = hass.states.get(group_entity_id)
assert group_state.state == STATE_OFF
dev1_cluster_on_off.request.reset_mock()
dev1_cluster_color.request.reset_mock()
dev1_cluster_level.request.reset_mock()
# test no transition provided and color temp while turning light on (color_provided_while_off)
await hass.services.async_call(
LIGHT_DOMAIN,
"turn_on",
{
"entity_id": device_1_entity_id,
"brightness": 25,
"color_temp": 236,
},
blocking=True,
)
assert dev1_cluster_on_off.request.call_count == 0
assert dev1_cluster_on_off.request.await_count == 0
assert dev1_cluster_color.request.call_count == 1
assert dev1_cluster_color.request.await_count == 1
assert dev1_cluster_level.request.call_count == 2
assert dev1_cluster_level.request.await_count == 2
# first it comes on with no transition at 2 brightness
assert dev1_cluster_level.request.call_args_list[0] == call(
False,
4,
dev1_cluster_level.commands_by_name["move_to_level_with_on_off"].schema,
2, # brightness (level in ZCL)
0, # transition time (ZCL time in 10ths of a second)
expect_reply=True,
manufacturer=None,
tries=1,
tsn=None,
)
assert dev1_cluster_color.request.call_args == call(
False,
10,
dev1_cluster_color.commands_by_name["move_to_color_temp"].schema,
236, # color temp mireds
0, # transition time (ZCL time in 10ths of a second) - no transition when color_provided_while_off
expect_reply=True,
manufacturer=None,
tries=1,
tsn=None,
)
assert dev1_cluster_level.request.call_args_list[1] == call(
False,
0,
dev1_cluster_level.commands_by_name["move_to_level"].schema,
25, # brightness (level in ZCL)
0, # transition time (ZCL time in 10ths of a second)
expect_reply=True,
manufacturer=None,
tries=1,
tsn=None,
)
light1_state = hass.states.get(device_1_entity_id)
assert light1_state.state == STATE_ON
assert light1_state.attributes["brightness"] == 25
assert light1_state.attributes["color_temp"] == 236
assert light1_state.attributes["color_mode"] == ColorMode.COLOR_TEMP
dev1_cluster_level.request.reset_mock()
dev1_cluster_color.request.reset_mock()
# turn light 1 back off to setup group test
await hass.services.async_call(
LIGHT_DOMAIN,
"turn_off",
{
"entity_id": device_1_entity_id,
},
blocking=True,
)
assert dev1_cluster_on_off.request.call_count == 1
assert dev1_cluster_on_off.request.await_count == 1
assert dev1_cluster_color.request.call_count == 0
assert dev1_cluster_color.request.await_count == 0
assert dev1_cluster_level.request.call_count == 0
assert dev1_cluster_level.request.await_count == 0
group_state = hass.states.get(group_entity_id)
assert group_state.state == STATE_OFF
dev1_cluster_on_off.request.reset_mock()
dev1_cluster_color.request.reset_mock()
dev1_cluster_level.request.reset_mock()
# test no transition when the same color temp is provided from off
await hass.services.async_call(
LIGHT_DOMAIN,
"turn_on",
{
"entity_id": device_1_entity_id,
"color_temp": 236,
},
blocking=True,
)
assert dev1_cluster_on_off.request.call_count == 1
assert dev1_cluster_on_off.request.await_count == 1
assert dev1_cluster_color.request.call_count == 1
assert dev1_cluster_color.request.await_count == 1
assert dev1_cluster_level.request.call_count == 0
assert dev1_cluster_level.request.await_count == 0
assert dev1_cluster_on_off.request.call_args == call(
False,
1,
dev1_cluster_on_off.commands_by_name["on"].schema,
expect_reply=True,
manufacturer=None,
tries=1,
tsn=None,
)
assert dev1_cluster_color.request.call_args == call(
False,
10,
dev1_cluster_color.commands_by_name["move_to_color_temp"].schema,
236, # color temp mireds
0, # transition time (ZCL time in 10ths of a second) - no transition when color_provided_while_off
expect_reply=True,
manufacturer=None,
tries=1,
tsn=None,
)
light1_state = hass.states.get(device_1_entity_id)
assert light1_state.state == STATE_ON
assert light1_state.attributes["brightness"] == 25
assert light1_state.attributes["color_temp"] == 236
assert light1_state.attributes["color_mode"] == ColorMode.COLOR_TEMP
dev1_cluster_on_off.request.reset_mock()
dev1_cluster_color.request.reset_mock()
# turn light 1 back off to setup group test
await hass.services.async_call(
LIGHT_DOMAIN,
"turn_off",
{
"entity_id": device_1_entity_id,
},
blocking=True,
)
assert dev1_cluster_on_off.request.call_count == 1
assert dev1_cluster_on_off.request.await_count == 1
assert dev1_cluster_color.request.call_count == 0
assert dev1_cluster_color.request.await_count == 0
assert dev1_cluster_level.request.call_count == 0
assert dev1_cluster_level.request.await_count == 0
group_state = hass.states.get(group_entity_id)
assert group_state.state == STATE_OFF
dev1_cluster_on_off.request.reset_mock()
dev1_cluster_color.request.reset_mock()
dev1_cluster_level.request.reset_mock()
# test sengled light uses default minimum transition time
dev2_cluster_on_off.request.reset_mock()
dev2_cluster_color.request.reset_mock()
dev2_cluster_level.request.reset_mock()
await hass.services.async_call(
LIGHT_DOMAIN,
"turn_on",
{"entity_id": device_2_entity_id, "transition": 0, "brightness": 100},
blocking=True,
)
assert dev2_cluster_on_off.request.call_count == 0
assert dev2_cluster_on_off.request.await_count == 0
assert dev2_cluster_color.request.call_count == 0
assert dev2_cluster_color.request.await_count == 0
assert dev2_cluster_level.request.call_count == 1
assert dev2_cluster_level.request.await_count == 1
assert dev2_cluster_level.request.call_args == call(
False,
4,
dev2_cluster_level.commands_by_name["move_to_level_with_on_off"].schema,
100, # brightness (level in ZCL)
1, # transition time - sengled light uses default minimum
expect_reply=True,
manufacturer=None,
tries=1,
tsn=None,
)
light2_state = hass.states.get(device_2_entity_id)
assert light2_state.state == STATE_ON
assert light2_state.attributes["brightness"] == 100
dev2_cluster_level.request.reset_mock()
# turn the sengled light back off
await hass.services.async_call(
LIGHT_DOMAIN,
"turn_off",
{
"entity_id": device_2_entity_id,
},
blocking=True,
)
assert dev2_cluster_on_off.request.call_count == 1
assert dev2_cluster_on_off.request.await_count == 1
assert dev2_cluster_color.request.call_count == 0
assert dev2_cluster_color.request.await_count == 0
assert dev2_cluster_level.request.call_count == 0
assert dev2_cluster_level.request.await_count == 0
light2_state = hass.states.get(device_2_entity_id)
assert light2_state.state == STATE_OFF
dev2_cluster_on_off.request.reset_mock()
# test non 0 length transition and color temp while turning light on and sengled (color_provided_while_off)
await hass.services.async_call(
LIGHT_DOMAIN,
"turn_on",
{
"entity_id": device_2_entity_id,
"transition": 1,
"brightness": 25,
"color_temp": 235,
},
blocking=True,
)
assert dev2_cluster_on_off.request.call_count == 0
assert dev2_cluster_on_off.request.await_count == 0
assert dev2_cluster_color.request.call_count == 1
assert dev2_cluster_color.request.await_count == 1
assert dev2_cluster_level.request.call_count == 2
assert dev2_cluster_level.request.await_count == 2
# first it comes on with no transition at 2 brightness
assert dev2_cluster_level.request.call_args_list[0] == call(
False,
4,
dev2_cluster_level.commands_by_name["move_to_level_with_on_off"].schema,
2, # brightness (level in ZCL)
1, # transition time (ZCL time in 10ths of a second)
expect_reply=True,
manufacturer=None,
tries=1,
tsn=None,
)
assert dev2_cluster_color.request.call_args == call(
False,
10,
dev2_cluster_color.commands_by_name["move_to_color_temp"].schema,
235, # color temp mireds
1, # transition time (ZCL time in 10ths of a second) - sengled transition == 1 when color_provided_while_off
expect_reply=True,
manufacturer=None,
tries=1,
tsn=None,
)
assert dev2_cluster_level.request.call_args_list[1] == call(
False,
0,
dev2_cluster_level.commands_by_name["move_to_level"].schema,
25, # brightness (level in ZCL)
10.0, # transition time (ZCL time in 10ths of a second)
expect_reply=True,
manufacturer=None,
tries=1,
tsn=None,
)
light2_state = hass.states.get(device_2_entity_id)
assert light2_state.state == STATE_ON
assert light2_state.attributes["brightness"] == 25
assert light2_state.attributes["color_temp"] == 235
assert light2_state.attributes["color_mode"] == ColorMode.COLOR_TEMP
dev2_cluster_level.request.reset_mock()
dev2_cluster_color.request.reset_mock()
# turn the sengled light back off
await hass.services.async_call(
LIGHT_DOMAIN,
"turn_off",
{
"entity_id": device_2_entity_id,
},
blocking=True,
)
assert dev2_cluster_on_off.request.call_count == 1
assert dev2_cluster_on_off.request.await_count == 1
assert dev2_cluster_color.request.call_count == 0
assert dev2_cluster_color.request.await_count == 0
assert dev2_cluster_level.request.call_count == 0
assert dev2_cluster_level.request.await_count == 0
light2_state = hass.states.get(device_2_entity_id)
assert light2_state.state == STATE_OFF
dev2_cluster_on_off.request.reset_mock()
# test non 0 length transition and color temp while turning group light on (color_provided_while_off)
await hass.services.async_call(
LIGHT_DOMAIN,
"turn_on",
{
"entity_id": group_entity_id,
"transition": 1,
"brightness": 25,
"color_temp": 235,
},
blocking=True,
)
group_on_off_channel = zha_group.endpoint[general.OnOff.cluster_id]
group_level_channel = zha_group.endpoint[general.LevelControl.cluster_id]
group_color_channel = zha_group.endpoint[lighting.Color.cluster_id]
assert group_on_off_channel.request.call_count == 0
assert group_on_off_channel.request.await_count == 0
assert group_color_channel.request.call_count == 1
assert group_color_channel.request.await_count == 1
assert group_level_channel.request.call_count == 1
assert group_level_channel.request.await_count == 1
# groups are omitted from the 3 call dance for color_provided_while_off
assert group_color_channel.request.call_args == call(
False,
10,
dev2_cluster_color.commands_by_name["move_to_color_temp"].schema,
235, # color temp mireds
10.0, # transition time (ZCL time in 10ths of a second) - sengled transition == 1 when color_provided_while_off
expect_reply=True,
manufacturer=None,
tries=1,
tsn=None,
)
assert group_level_channel.request.call_args == call(
False,
4,
dev2_cluster_level.commands_by_name["move_to_level_with_on_off"].schema,
25, # brightness (level in ZCL)
10.0, # transition time (ZCL time in 10ths of a second)
expect_reply=True,
manufacturer=None,
tries=1,
tsn=None,
)
group_state = hass.states.get(group_entity_id)
assert group_state.state == STATE_ON
assert group_state.attributes["brightness"] == 25
assert group_state.attributes["color_temp"] == 235
assert group_state.attributes["color_mode"] == ColorMode.COLOR_TEMP
group_on_off_channel.request.reset_mock()
group_color_channel.request.reset_mock()
group_level_channel.request.reset_mock()
# turn the sengled light back on
await hass.services.async_call(
LIGHT_DOMAIN,
"turn_on",
{
"entity_id": device_2_entity_id,
},
blocking=True,
)
assert dev2_cluster_on_off.request.call_count == 1
assert dev2_cluster_on_off.request.await_count == 1
assert dev2_cluster_color.request.call_count == 0
assert dev2_cluster_color.request.await_count == 0
assert dev2_cluster_level.request.call_count == 0
assert dev2_cluster_level.request.await_count == 0
light2_state = hass.states.get(device_2_entity_id)
assert light2_state.state == STATE_ON
dev2_cluster_on_off.request.reset_mock()
# turn the light off with a transition
await hass.services.async_call(
LIGHT_DOMAIN,
"turn_off",
{"entity_id": device_2_entity_id, "transition": 2},
blocking=True,
)
assert dev2_cluster_on_off.request.call_count == 0
assert dev2_cluster_on_off.request.await_count == 0
assert dev2_cluster_color.request.call_count == 0
assert dev2_cluster_color.request.await_count == 0
assert dev2_cluster_level.request.call_count == 1
assert dev2_cluster_level.request.await_count == 1
assert dev2_cluster_level.request.call_args == call(
False,
4,
dev2_cluster_level.commands_by_name["move_to_level_with_on_off"].schema,
0, # brightness (level in ZCL)
20, # transition time
expect_reply=True,
manufacturer=None,
tries=1,
tsn=None,
)
light2_state = hass.states.get(device_2_entity_id)
assert light2_state.state == STATE_OFF
dev2_cluster_level.request.reset_mock()
# turn the light back on with no args should use a transition and last known brightness
await hass.services.async_call(
LIGHT_DOMAIN,
"turn_on",
{"entity_id": device_2_entity_id},
blocking=True,
)
assert dev2_cluster_on_off.request.call_count == 0
assert dev2_cluster_on_off.request.await_count == 0
assert dev2_cluster_color.request.call_count == 0
assert dev2_cluster_color.request.await_count == 0
assert dev2_cluster_level.request.call_count == 1
assert dev2_cluster_level.request.await_count == 1
assert dev2_cluster_level.request.call_args == call(
False,
4,
dev2_cluster_level.commands_by_name["move_to_level_with_on_off"].schema,
25, # brightness (level in ZCL) - this is the last brightness we set a few tests above
1, # transition time - sengled light uses default minimum
expect_reply=True,
manufacturer=None,
tries=1,
tsn=None,
)
light2_state = hass.states.get(device_2_entity_id)
assert light2_state.state == STATE_ON
dev2_cluster_level.request.reset_mock()
# test eWeLink color temp while turning light on from off (color_provided_while_off)
await hass.services.async_call(
LIGHT_DOMAIN,
"turn_on",
{
"entity_id": eWeLink_light_entity_id,
"color_temp": 235,
},
blocking=True,
)
assert eWeLink_cluster_on_off.request.call_count == 1
assert eWeLink_cluster_on_off.request.await_count == 1
assert eWeLink_cluster_color.request.call_count == 1
assert eWeLink_cluster_color.request.await_count == 1
assert eWeLink_cluster_level.request.call_count == 0
assert eWeLink_cluster_level.request.await_count == 0
# first it comes on
assert eWeLink_cluster_on_off.request.call_args_list[0] == call(
False,
1,
eWeLink_cluster_on_off.commands_by_name["on"].schema,
expect_reply=True,
manufacturer=None,
tries=1,
tsn=None,
)
assert dev1_cluster_color.request.call_args == call(
False,
10,
dev1_cluster_color.commands_by_name["move_to_color_temp"].schema,
235, # color temp mireds
0, # transition time (ZCL time in 10ths of a second)
expect_reply=True,
manufacturer=None,
tries=1,
tsn=None,
)
eWeLink_state = hass.states.get(eWeLink_light_entity_id)
assert eWeLink_state.state == STATE_ON
assert eWeLink_state.attributes["color_temp"] == 235
assert eWeLink_state.attributes["color_mode"] == ColorMode.COLOR_TEMP
async def async_test_on_off_from_light(hass, cluster, entity_id):
"""Test on off functionality from the light."""
# turn on at light
@ -463,7 +1260,7 @@ async def async_test_level_on_off_from_hass(
4,
level_cluster.commands_by_name["move_to_level_with_on_off"].schema,
10,
1,
0,
expect_reply=True,
manufacturer=None,
tries=1,
@ -601,7 +1398,10 @@ async def test_zha_group_light_entity(
# test that the lights were created and are off
group_state = hass.states.get(group_entity_id)
assert group_state.state == STATE_OFF
assert group_state.attributes["supported_color_modes"] == [ColorMode.HS]
assert group_state.attributes["supported_color_modes"] == [
ColorMode.COLOR_TEMP,
ColorMode.HS,
]
# Light which is off has no color mode
assert "color_mode" not in group_state.attributes
@ -629,7 +1429,10 @@ async def test_zha_group_light_entity(
# Check state
group_state = hass.states.get(group_entity_id)
assert group_state.state == STATE_ON
assert group_state.attributes["supported_color_modes"] == [ColorMode.HS]
assert group_state.attributes["supported_color_modes"] == [
ColorMode.COLOR_TEMP,
ColorMode.HS,
]
assert group_state.attributes["color_mode"] == ColorMode.HS
# test long flashing the lights from the HA

View File

@ -2,6 +2,7 @@
import asyncio
from datetime import timedelta
import json
from typing import NamedTuple
from unittest.mock import Mock, patch
import pytest
@ -13,8 +14,9 @@ from homeassistant.const import (
from homeassistant.core import CoreState
from homeassistant.helpers import storage
from homeassistant.util import dt
from homeassistant.util.color import RGBColor
from tests.common import async_fire_time_changed
from tests.common import async_fire_time_changed, async_test_home_assistant
MOCK_VERSION = 1
MOCK_VERSION_2 = 2
@ -460,3 +462,47 @@ async def test_changing_delayed_written_data(hass, store, hass_storage):
"key": MOCK_KEY,
"data": {"hello": "world"},
}
async def test_saving_load_round_trip(tmpdir):
"""Test saving and loading round trip."""
loop = asyncio.get_running_loop()
hass = await async_test_home_assistant(loop)
hass.config.config_dir = await hass.async_add_executor_job(
tmpdir.mkdir, "temp_storage"
)
class NamedTupleSubclass(NamedTuple):
"""A NamedTuple subclass."""
name: str
nts = NamedTupleSubclass("a")
data = {
"named_tuple_subclass": nts,
"rgb_color": RGBColor(255, 255, 0),
"set": {1, 2, 3},
"list": [1, 2, 3],
"tuple": (1, 2, 3),
"dict_with_int": {1: 1, 2: 2},
"dict_with_named_tuple": {1: nts, 2: nts},
}
store = storage.Store(
hass, MOCK_VERSION_2, MOCK_KEY, minor_version=MOCK_MINOR_VERSION_1
)
await store.async_save(data)
load = await store.async_load()
assert load == {
"dict_with_int": {"1": 1, "2": 2},
"dict_with_named_tuple": {"1": ["a"], "2": ["a"]},
"list": [1, 2, 3],
"named_tuple_subclass": ["a"],
"rgb_color": [255, 255, 0],
"set": [1, 2, 3],
"tuple": [1, 2, 3],
}
await hass.async_stop(force=True)

View File

@ -5,13 +5,13 @@ from json import JSONEncoder, dumps
import math
import os
from tempfile import mkdtemp
from unittest.mock import Mock
from unittest.mock import Mock, patch
import pytest
from homeassistant.core import Event, State
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.template import TupleWrapper
from homeassistant.helpers.json import JSONEncoder as DefaultHASSJSONEncoder
from homeassistant.util.json import (
SerializationError,
find_paths_unserializable_data,
@ -82,23 +82,15 @@ def test_overwrite_and_reload(atomic_writes):
def test_save_bad_data():
"""Test error from trying to save unserializable data."""
class CannotSerializeMe:
"""Cannot serialize this."""
with pytest.raises(SerializationError) as excinfo:
save_json("test4", {"hello": set()})
save_json("test4", {"hello": CannotSerializeMe()})
assert (
"Failed to serialize to JSON: test4. Bad data at $.hello=set()(<class 'set'>"
in str(excinfo.value)
)
def test_save_bad_data_tuple_wrapper():
"""Test error from trying to save unserializable data."""
with pytest.raises(SerializationError) as excinfo:
save_json("test4", {"hello": TupleWrapper(("4", "5"))})
assert (
"Failed to serialize to JSON: test4. Bad data at $.hello=('4', '5')(<class 'homeassistant.helpers.template.TupleWrapper'>"
in str(excinfo.value)
assert "Failed to serialize to JSON: test4. Bad data at $.hello=" in str(
excinfo.value
)
@ -127,6 +119,21 @@ def test_custom_encoder():
assert data == "9"
def test_default_encoder_is_passed():
"""Test we use orjson if they pass in the default encoder."""
fname = _path_for("test6")
with patch(
"homeassistant.util.json.orjson.dumps", return_value=b"{}"
) as mock_orjson_dumps:
save_json(fname, {"any": 1}, encoder=DefaultHASSJSONEncoder)
assert len(mock_orjson_dumps.mock_calls) == 1
# Patch json.dumps to make sure we are using the orjson path
with patch("homeassistant.util.json.json.dumps", side_effect=Exception):
save_json(fname, {"any": {1}}, encoder=DefaultHASSJSONEncoder)
data = load_json(fname)
assert data == {"any": [1]}
def test_find_unserializable_data():
"""Find unserializeable data."""
assert find_paths_unserializable_data(1) == {}

View File

@ -0,0 +1,26 @@
- id: '1658085239190'
alias: Config validation test
description: ''
trigger:
- platform: time
at: 00:02:03
condition: []
action:
- service: script.notify_admin
data:
title: 'Here's something that does not work...!'
message: failing
mode: single
- id: '165808523911590'
alias: Config validation test FIXED
description: ''
trigger:
- platform: time
at: 00:02:03
condition: []
action:
- service: script.notify_admin
data:
title: 'Here is something that should work...!'
message: fixed?
mode: single

View File

@ -2,6 +2,7 @@
import importlib
import io
import os
import pathlib
import unittest
from unittest.mock import patch
@ -490,3 +491,12 @@ def test_input(try_both_loaders, try_both_dumpers):
def test_c_loader_is_available_in_ci():
"""Verify we are testing the C loader in the CI."""
assert yaml.loader.HAS_C_LOADER is True
async def test_loading_actual_file_with_syntax(hass, try_both_loaders):
"""Test loading a real file with syntax errors."""
with pytest.raises(HomeAssistantError):
fixture_path = pathlib.Path(__file__).parent.joinpath(
"fixtures", "bad.yaml.txt"
)
await hass.async_add_executor_job(load_yaml_config_file, fixture_path)