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 collections.abc import Mapping
from datetime import timedelta from datetime import timedelta
from math import ceil from math import ceil
from typing import Any, cast from typing import Any
from pyairvisual import CloudAPI, NodeSamba from pyairvisual import CloudAPI, NodeSamba
from pyairvisual.errors import ( from pyairvisual.errors import (
@ -12,6 +12,7 @@ from pyairvisual.errors import (
InvalidKeyError, InvalidKeyError,
KeyExpiredError, KeyExpiredError,
NodeProError, NodeProError,
UnauthorizedError,
) )
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
@ -210,9 +211,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
) )
try: try:
data = await api_coro return await api_coro
return cast(dict[str, Any], data) except (InvalidKeyError, KeyExpiredError, UnauthorizedError) as ex:
except (InvalidKeyError, KeyExpiredError) as ex:
raise ConfigEntryAuthFailed from ex raise ConfigEntryAuthFailed from ex
except AirVisualError as err: except AirVisualError as err:
raise UpdateFailed(f"Error while retrieving data: {err}") from 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( async with NodeSamba(
entry.data[CONF_IP_ADDRESS], entry.data[CONF_PASSWORD] entry.data[CONF_IP_ADDRESS], entry.data[CONF_PASSWORD]
) as node: ) as node:
data = await node.async_get_latest_measurements() return await node.async_get_latest_measurements()
return cast(dict[str, Any], data)
except NodeProError as err: except NodeProError as err:
raise UpdateFailed(f"Error while retrieving data: {err}") from 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 ( from pyairvisual.errors import (
AirVisualError, AirVisualError,
InvalidKeyError, InvalidKeyError,
KeyExpiredError,
NodeProError, NodeProError,
NotFoundError, NotFoundError,
UnauthorizedError,
) )
import voluptuous as vol 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: if user_input[CONF_API_KEY] not in valid_keys:
try: try:
await coro await coro
except InvalidKeyError: except (InvalidKeyError, KeyExpiredError, UnauthorizedError):
errors[CONF_API_KEY] = "invalid_api_key" errors[CONF_API_KEY] = "invalid_api_key"
except NotFoundError: except NotFoundError:
errors[CONF_CITY] = "location_not_found" errors[CONF_CITY] = "location_not_found"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -87,7 +87,7 @@ class ArubaDeviceScanner(DeviceScanner):
def get_aruba_data(self): def get_aruba_data(self):
"""Retrieve data from Aruba Access Point and return parsed result.""" """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) ssh = pexpect.spawn(connect)
query = ssh.expect( query = ssh.expect(
[ [

View File

@ -33,7 +33,8 @@ class BMWDataUpdateCoordinator(DataUpdateCoordinator):
entry.data[CONF_PASSWORD], entry.data[CONF_PASSWORD],
get_region_from_name(entry.data[CONF_REGION]), get_region_from_name(entry.data[CONF_REGION]),
observer_position=GPSPosition(hass.config.latitude, hass.config.longitude), 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.read_only = entry.options[CONF_READ_ONLY]
self._entry = entry self._entry = entry

View File

@ -2,7 +2,7 @@
"domain": "bmw_connected_drive", "domain": "bmw_connected_drive",
"name": "BMW Connected Drive", "name": "BMW Connected Drive",
"documentation": "https://www.home-assistant.io/integrations/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"], "codeowners": ["@gerard33", "@rikroe"],
"config_flow": true, "config_flow": true,
"iot_class": "cloud_polling", "iot_class": "cloud_polling",

View File

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

View File

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

View File

@ -44,13 +44,15 @@ class HiveDeviceLight(HiveEntity, LightEntity):
super().__init__(hive, hive_device) super().__init__(hive, hive_device)
if self.device["hiveType"] == "warmwhitelight": if self.device["hiveType"] == "warmwhitelight":
self._attr_supported_color_modes = {ColorMode.BRIGHTNESS} self._attr_supported_color_modes = {ColorMode.BRIGHTNESS}
self._attr_color_mode = ColorMode.BRIGHTNESS
elif self.device["hiveType"] == "tuneablelight": elif self.device["hiveType"] == "tuneablelight":
self._attr_supported_color_modes = {ColorMode.COLOR_TEMP} self._attr_supported_color_modes = {ColorMode.COLOR_TEMP}
self._attr_color_mode = ColorMode.COLOR_TEMP
elif self.device["hiveType"] == "colourtuneablelight": elif self.device["hiveType"] == "colourtuneablelight":
self._attr_supported_color_modes = {ColorMode.COLOR_TEMP, ColorMode.HS} self._attr_supported_color_modes = {ColorMode.COLOR_TEMP, ColorMode.HS}
self._attr_min_mireds = self.device.get("min_mireds") self._attr_min_mireds = 153
self._attr_max_mireds = self.device.get("max_mireds") self._attr_max_mireds = 370
@refresh_system @refresh_system
async def async_turn_on(self, **kwargs): async def async_turn_on(self, **kwargs):
@ -94,6 +96,13 @@ class HiveDeviceLight(HiveEntity, LightEntity):
if self._attr_available: if self._attr_available:
self._attr_is_on = self.device["status"]["state"] self._attr_is_on = self.device["status"]["state"]
self._attr_brightness = self.device["status"]["brightness"] 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["hiveType"] == "colourtuneablelight":
rgb = self.device["status"]["hs_color"] if self.device["status"]["mode"] == "COLOUR":
self._attr_hs_color = color_util.color_RGB_to_hs(*rgb) 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.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA
from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE 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 homeassistant.helpers.typing import ConfigType
from .const import DOMAIN, KNOWN_DEVICES, TRIGGERS from .const import DOMAIN, KNOWN_DEVICES, TRIGGERS
@ -86,13 +86,13 @@ class TriggerSource:
) -> CALLBACK_TYPE: ) -> CALLBACK_TYPE:
"""Attach a trigger.""" """Attach a trigger."""
trigger_data = automation_info["trigger_data"] trigger_data = automation_info["trigger_data"]
job = HassJob(action)
@callback
def event_handler(char): def event_handler(char):
if config[CONF_SUBTYPE] != HK_TO_HA_INPUT_EVENT_VALUES[char["value"]]: if config[CONF_SUBTYPE] != HK_TO_HA_INPUT_EVENT_VALUES[char["value"]]:
return return
self._hass.async_create_task( self._hass.async_run_hass_job(job, {"trigger": {**trigger_data, **config}})
action({"trigger": {**trigger_data, **config}})
)
trigger = self._triggers[config[CONF_TYPE], config[CONF_SUBTYPE]] trigger = self._triggers[config[CONF_TYPE], config[CONF_SUBTYPE]]
iid = trigger["characteristic"] 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]): def async_fire_triggers(conn: HKDevice, events: dict[tuple[int, int], Any]):
"""Process events generated by a HomeKit accessory into automation triggers.""" """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(): for (aid, iid), ev in events.items():
if aid in conn.devices: if aid in conn.devices:
device_id = conn.devices[aid] device_id = conn.devices[aid]
if device_id in conn.hass.data[TRIGGERS]: if source := trigger_sources.get(device_id):
source = conn.hass.data[TRIGGERS][device_id]
source.fire(iid, ev) source.fire(iid, ev)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

@ -11,6 +11,10 @@ import orjson
from homeassistant.core import Event, State from homeassistant.core import Event, State
from homeassistant.exceptions import HomeAssistantError 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 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 return {} if default is None else default
def _orjson_encoder(data: Any) -> str: def _orjson_default_encoder(data: Any) -> str:
"""JSON encoder that uses orjson.""" """JSON encoder that uses orjson with hass defaults."""
return orjson.dumps( 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") ).decode("utf-8")
@ -64,13 +70,19 @@ def save_json(
Returns True on success. Returns True on success.
""" """
dump: Callable[[Any], Any] = json.dumps dump: Callable[[Any], Any]
try: 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) json_data = json.dumps(data, indent=2, cls=encoder)
else: else:
dump = _orjson_encoder dump = _orjson_default_encoder
json_data = _orjson_encoder(data) json_data = _orjson_default_encoder(data)
except TypeError as error: 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))}" msg = f"Failed to serialize to JSON: {filename}. Bad data at {format_unserializable_data(find_paths_unserializable_data(data, dump=dump))}"
_LOGGER.error(msg) _LOGGER.error(msg)

View File

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

View File

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

View File

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

View File

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

View File

@ -4,8 +4,10 @@ from unittest.mock import patch
from pyairvisual.errors import ( from pyairvisual.errors import (
AirVisualError, AirVisualError,
InvalidKeyError, InvalidKeyError,
KeyExpiredError,
NodeProError, NodeProError,
NotFoundError, NotFoundError,
UnauthorizedError,
) )
import pytest import pytest
@ -84,6 +86,28 @@ async def test_duplicate_error(hass, config, config_entry, data):
{CONF_API_KEY: "invalid_api_key"}, {CONF_API_KEY: "invalid_api_key"},
INTEGRATION_TYPE_GEOGRAPHY_NAME, 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", CONF_API_KEY: "abcde12345",

View File

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

View File

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

View File

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

View File

@ -34,7 +34,7 @@ async def test_device_registry_insert(hass):
with patch( with patch(
"homeassistant.components.opentherm_gw.OpenThermGatewayDevice.cleanup", "homeassistant.components.opentherm_gw.OpenThermGatewayDevice.cleanup",
return_value=None, 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 setup.async_setup_component(hass, DOMAIN, {})
await hass.async_block_till_done() await hass.async_block_till_done()
@ -62,7 +62,7 @@ async def test_device_registry_update(hass):
with patch( with patch(
"homeassistant.components.opentherm_gw.OpenThermGatewayDevice.cleanup", "homeassistant.components.opentherm_gw.OpenThermGatewayDevice.cleanup",
return_value=None, 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 setup.async_setup_component(hass, DOMAIN, {})
await hass.async_block_till_done() await hass.async_block_till_done()

View File

@ -9,14 +9,18 @@ import aiohttp
from pyunifiprotect import NotAuthorized, NvrError, ProtectApiClient from pyunifiprotect import NotAuthorized, NvrError, ProtectApiClient
from pyunifiprotect.data import NVR, Bootstrap, Light 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.config_entries import ConfigEntry, ConfigEntryState
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from . import _patch_discovery from . import _patch_discovery
from .utils import MockUFPFixture, init_entry from .utils import MockUFPFixture, init_entry, time_changed
from tests.common import MockConfigEntry 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): async def test_setup_failed_update_reauth(hass: HomeAssistant, ufp: MockUFPFixture):
"""Test setup of unifiprotect entry with update that gives unauthroized error.""" """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.config_entries.async_setup(ufp.entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
assert ufp.entry.state == ConfigEntryState.SETUP_RETRY assert ufp.entry.state == ConfigEntryState.LOADED
assert ufp.api.update.called
# 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): async def test_setup_failed_error(hass: HomeAssistant, ufp: MockUFPFixture):

View File

@ -15,7 +15,11 @@ from homeassistant.components.light import (
ColorMode, ColorMode,
) )
from homeassistant.components.zha.core.group import GroupMember 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 from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE, Platform
import homeassistant.util.dt as dt_util 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, ieee=IEEE_GROUPABLE_DEVICE,
nwk=0xB79D, 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 = await zha_device_joined(zigpy_device)
zha_device.available = True zha_device.available = True
return zha_device return zha_device
@ -167,8 +175,13 @@ async def device_light_2(hass, zigpy_device_mock, zha_device_joined):
} }
}, },
ieee=IEEE_GROUPABLE_DEVICE2, ieee=IEEE_GROUPABLE_DEVICE2,
manufacturer="Sengled",
nwk=0xC79E, 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 = await zha_device_joined(zigpy_device)
zha_device.available = True zha_device.available = True
return zha_device return zha_device
@ -201,6 +214,38 @@ async def device_light_3(hass, zigpy_device_mock, zha_device_joined):
return zha_device 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): async def test_light_refresh(hass, zigpy_device_mock, zha_device_joined_restored):
"""Test zha light platform refresh.""" """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) 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): async def async_test_on_off_from_light(hass, cluster, entity_id):
"""Test on off functionality from the light.""" """Test on off functionality from the light."""
# turn on at light # turn on at light
@ -463,7 +1260,7 @@ async def async_test_level_on_off_from_hass(
4, 4,
level_cluster.commands_by_name["move_to_level_with_on_off"].schema, level_cluster.commands_by_name["move_to_level_with_on_off"].schema,
10, 10,
1, 0,
expect_reply=True, expect_reply=True,
manufacturer=None, manufacturer=None,
tries=1, tries=1,
@ -601,7 +1398,10 @@ async def test_zha_group_light_entity(
# test that the lights were created and are off # test that the lights were created and are off
group_state = hass.states.get(group_entity_id) group_state = hass.states.get(group_entity_id)
assert group_state.state == STATE_OFF 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 # Light which is off has no color mode
assert "color_mode" not in group_state.attributes assert "color_mode" not in group_state.attributes
@ -629,7 +1429,10 @@ async def test_zha_group_light_entity(
# Check state # Check state
group_state = hass.states.get(group_entity_id) group_state = hass.states.get(group_entity_id)
assert group_state.state == STATE_ON 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 assert group_state.attributes["color_mode"] == ColorMode.HS
# test long flashing the lights from the HA # test long flashing the lights from the HA

View File

@ -2,6 +2,7 @@
import asyncio import asyncio
from datetime import timedelta from datetime import timedelta
import json import json
from typing import NamedTuple
from unittest.mock import Mock, patch from unittest.mock import Mock, patch
import pytest import pytest
@ -13,8 +14,9 @@ from homeassistant.const import (
from homeassistant.core import CoreState from homeassistant.core import CoreState
from homeassistant.helpers import storage from homeassistant.helpers import storage
from homeassistant.util import dt 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 = 1
MOCK_VERSION_2 = 2 MOCK_VERSION_2 = 2
@ -460,3 +462,47 @@ async def test_changing_delayed_written_data(hass, store, hass_storage):
"key": MOCK_KEY, "key": MOCK_KEY,
"data": {"hello": "world"}, "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 math
import os import os
from tempfile import mkdtemp from tempfile import mkdtemp
from unittest.mock import Mock from unittest.mock import Mock, patch
import pytest import pytest
from homeassistant.core import Event, State from homeassistant.core import Event, State
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.template import TupleWrapper from homeassistant.helpers.json import JSONEncoder as DefaultHASSJSONEncoder
from homeassistant.util.json import ( from homeassistant.util.json import (
SerializationError, SerializationError,
find_paths_unserializable_data, find_paths_unserializable_data,
@ -82,23 +82,15 @@ def test_overwrite_and_reload(atomic_writes):
def test_save_bad_data(): def test_save_bad_data():
"""Test error from trying to save unserializable data.""" """Test error from trying to save unserializable data."""
class CannotSerializeMe:
"""Cannot serialize this."""
with pytest.raises(SerializationError) as excinfo: with pytest.raises(SerializationError) as excinfo:
save_json("test4", {"hello": set()}) save_json("test4", {"hello": CannotSerializeMe()})
assert ( assert "Failed to serialize to JSON: test4. Bad data at $.hello=" in str(
"Failed to serialize to JSON: test4. Bad data at $.hello=set()(<class 'set'>" excinfo.value
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)
) )
@ -127,6 +119,21 @@ def test_custom_encoder():
assert data == "9" 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(): def test_find_unserializable_data():
"""Find unserializeable data.""" """Find unserializeable data."""
assert find_paths_unserializable_data(1) == {} 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 importlib
import io import io
import os import os
import pathlib
import unittest import unittest
from unittest.mock import patch 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(): def test_c_loader_is_available_in_ci():
"""Verify we are testing the C loader in the CI.""" """Verify we are testing the C loader in the CI."""
assert yaml.loader.HAS_C_LOADER is True 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)