Merge pull request #63867 from home-assistant/rc

This commit is contained in:
Paulus Schoutsen 2022-01-10 17:26:09 -08:00 committed by GitHub
commit b6f432645d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 793 additions and 191 deletions

View File

@ -452,7 +452,7 @@ homeassistant/components/samsungtv/* @escoand @chemelli74
homeassistant/components/scene/* @home-assistant/core homeassistant/components/scene/* @home-assistant/core
homeassistant/components/schluter/* @prairieapps homeassistant/components/schluter/* @prairieapps
homeassistant/components/scrape/* @fabaff homeassistant/components/scrape/* @fabaff
homeassistant/components/screenlogic/* @dieselrabbit homeassistant/components/screenlogic/* @dieselrabbit @bdraco
homeassistant/components/script/* @home-assistant/core homeassistant/components/script/* @home-assistant/core
homeassistant/components/search/* @home-assistant/core homeassistant/components/search/* @home-assistant/core
homeassistant/components/select/* @home-assistant/core homeassistant/components/select/* @home-assistant/core

View File

@ -245,6 +245,15 @@ class AugustData(AugustSubscriberMixin):
device_id, device_id,
) )
async def async_lock_async(self, device_id):
"""Lock the device but do not wait for a response since it will come via pubnub."""
return await self._async_call_api_op_requires_bridge(
device_id,
self._api.async_lock_async,
self._august_gateway.access_token,
device_id,
)
async def async_unlock(self, device_id): async def async_unlock(self, device_id):
"""Unlock the device.""" """Unlock the device."""
return await self._async_call_api_op_requires_bridge( return await self._async_call_api_op_requires_bridge(
@ -254,6 +263,15 @@ class AugustData(AugustSubscriberMixin):
device_id, device_id,
) )
async def async_unlock_async(self, device_id):
"""Unlock the device but do not wait for a response since it will come via pubnub."""
return await self._async_call_api_op_requires_bridge(
device_id,
self._api.async_unlock_async,
self._august_gateway.access_token,
device_id,
)
async def _async_call_api_op_requires_bridge( async def _async_call_api_op_requires_bridge(
self, device_id, func, *args, **kwargs self, device_id, func, *args, **kwargs
): ):

View File

@ -61,6 +61,17 @@ def _retrieve_motion_state(data: AugustData, detail: DoorbellDetail) -> bool:
return _activity_time_based_state(latest) return _activity_time_based_state(latest)
def _retrieve_image_capture_state(data: AugustData, detail: DoorbellDetail) -> bool:
latest = data.activity_stream.get_latest_device_activity(
detail.device_id, {ActivityType.DOORBELL_IMAGE_CAPTURE}
)
if latest is None:
return False
return _activity_time_based_state(latest)
def _retrieve_ding_state(data: AugustData, detail: DoorbellDetail) -> bool: def _retrieve_ding_state(data: AugustData, detail: DoorbellDetail) -> bool:
latest = data.activity_stream.get_latest_device_activity( latest = data.activity_stream.get_latest_device_activity(
detail.device_id, {ActivityType.DOORBELL_DING} detail.device_id, {ActivityType.DOORBELL_DING}
@ -126,6 +137,13 @@ SENSOR_TYPES_DOORBELL: tuple[AugustBinarySensorEntityDescription, ...] = (
value_fn=_retrieve_motion_state, value_fn=_retrieve_motion_state,
is_time_based=True, is_time_based=True,
), ),
AugustBinarySensorEntityDescription(
key="doorbell_image_capture",
name="Image Capture",
icon="mdi:file-image",
value_fn=_retrieve_image_capture_state,
is_time_based=True,
),
AugustBinarySensorEntityDescription( AugustBinarySensorEntityDescription(
key="doorbell_online", key="doorbell_online",
name="Online", name="Online",

View File

@ -63,7 +63,8 @@ class AugustCamera(AugustEntityMixin, Camera):
def _update_from_data(self): def _update_from_data(self):
"""Get the latest state of the sensor.""" """Get the latest state of the sensor."""
doorbell_activity = self._data.activity_stream.get_latest_device_activity( doorbell_activity = self._data.activity_stream.get_latest_device_activity(
self._device_id, {ActivityType.DOORBELL_MOTION} self._device_id,
{ActivityType.DOORBELL_MOTION, ActivityType.DOORBELL_IMAGE_CAPTURE},
) )
if doorbell_activity is not None: if doorbell_activity is not None:

View File

@ -41,10 +41,16 @@ class AugustLock(AugustEntityMixin, RestoreEntity, LockEntity):
async def async_lock(self, **kwargs): async def async_lock(self, **kwargs):
"""Lock the device.""" """Lock the device."""
if self._data.activity_stream.pubnub.connected:
await self._data.async_lock_async(self._device_id)
return
await self._call_lock_operation(self._data.async_lock) await self._call_lock_operation(self._data.async_lock)
async def async_unlock(self, **kwargs): async def async_unlock(self, **kwargs):
"""Unlock the device.""" """Unlock the device."""
if self._data.activity_stream.pubnub.connected:
await self._data.async_unlock_async(self._device_id)
return
await self._call_lock_operation(self._data.async_unlock) await self._call_lock_operation(self._data.async_unlock)
async def _call_lock_operation(self, lock_operation): async def _call_lock_operation(self, lock_operation):

View File

@ -2,7 +2,7 @@
"domain": "august", "domain": "august",
"name": "August", "name": "August",
"documentation": "https://www.home-assistant.io/integrations/august", "documentation": "https://www.home-assistant.io/integrations/august",
"requirements": ["yalexs==1.1.13"], "requirements": ["yalexs==1.1.17"],
"codeowners": ["@bdraco"], "codeowners": ["@bdraco"],
"dhcp": [ "dhcp": [
{ {

View File

@ -3,7 +3,7 @@
"name": "Google Cast", "name": "Google Cast",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/cast", "documentation": "https://www.home-assistant.io/integrations/cast",
"requirements": ["pychromecast==10.2.2"], "requirements": ["pychromecast==10.2.3"],
"after_dependencies": [ "after_dependencies": [
"cloud", "cloud",
"http", "http",

View File

@ -21,6 +21,7 @@ from homeassistant.components.google_assistant import helpers as google_helpers
from homeassistant.components.http import HomeAssistantView from homeassistant.components.http import HomeAssistantView
from homeassistant.components.http.data_validator import RequestDataValidator from homeassistant.components.http.data_validator import RequestDataValidator
from homeassistant.components.websocket_api import const as ws_const from homeassistant.components.websocket_api import const as ws_const
from homeassistant.util.location import async_detect_location_info
from .const import ( from .const import (
DOMAIN, DOMAIN,
@ -220,8 +221,23 @@ class CloudRegisterView(HomeAssistantView):
hass = request.app["hass"] hass = request.app["hass"]
cloud = hass.data[DOMAIN] cloud = hass.data[DOMAIN]
client_metadata = None
if location_info := await async_detect_location_info(
hass.helpers.aiohttp_client.async_get_clientsession()
):
client_metadata = {
"NC_COUNTRY_CODE": location_info.country_code,
"NC_REGION_CODE": location_info.region_code,
"NC_ZIP_CODE": location_info.zip_code,
}
async with async_timeout.timeout(REQUEST_TIMEOUT): async with async_timeout.timeout(REQUEST_TIMEOUT):
await cloud.auth.async_register(data["email"], data["password"]) await cloud.auth.async_register(
data["email"],
data["password"],
client_metadata=client_metadata,
)
return self.json_message("ok") return self.json_message("ok")

View File

@ -2,7 +2,7 @@
"domain": "cloud", "domain": "cloud",
"name": "Home Assistant Cloud", "name": "Home Assistant Cloud",
"documentation": "https://www.home-assistant.io/integrations/cloud", "documentation": "https://www.home-assistant.io/integrations/cloud",
"requirements": ["hass-nabucasa==0.50.0"], "requirements": ["hass-nabucasa==0.51.0"],
"dependencies": ["http", "webhook"], "dependencies": ["http", "webhook"],
"after_dependencies": ["google_assistant", "alexa"], "after_dependencies": ["google_assistant", "alexa"],
"codeowners": ["@home-assistant/cloud"], "codeowners": ["@home-assistant/cloud"],

View File

@ -3,7 +3,7 @@
"name": "Flux LED/MagicHome", "name": "Flux LED/MagicHome",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/flux_led", "documentation": "https://www.home-assistant.io/integrations/flux_led",
"requirements": ["flux_led==0.27.32"], "requirements": ["flux_led==0.27.45"],
"quality_scale": "platinum", "quality_scale": "platinum",
"codeowners": ["@icemanch"], "codeowners": ["@icemanch"],
"iot_class": "local_push", "iot_class": "local_push",

View File

@ -3,7 +3,7 @@
"name": "Home Assistant Frontend", "name": "Home Assistant Frontend",
"documentation": "https://www.home-assistant.io/integrations/frontend", "documentation": "https://www.home-assistant.io/integrations/frontend",
"requirements": [ "requirements": [
"home-assistant-frontend==20211229.0" "home-assistant-frontend==20211229.1"
], ],
"dependencies": [ "dependencies": [
"api", "api",

View File

@ -2,7 +2,7 @@
"domain": "harmony", "domain": "harmony",
"name": "Logitech Harmony Hub", "name": "Logitech Harmony Hub",
"documentation": "https://www.home-assistant.io/integrations/harmony", "documentation": "https://www.home-assistant.io/integrations/harmony",
"requirements": ["aioharmony==0.2.8"], "requirements": ["aioharmony==0.2.9"],
"codeowners": [ "codeowners": [
"@ehendrix23", "@ehendrix23",
"@bramkragten", "@bramkragten",

View File

@ -732,7 +732,12 @@ class HomeKit:
"""Remove all pairings for an accessory so it can be repaired.""" """Remove all pairings for an accessory so it can be repaired."""
state = self.driver.state state = self.driver.state
for client_uuid in list(state.paired_clients): for client_uuid in list(state.paired_clients):
state.remove_paired_client(client_uuid) # We need to check again since removing a single client
# can result in removing all the clients that the client
# granted access to if it was an admin, otherwise
# remove_paired_client can generate a KeyError
if client_uuid in state.paired_clients:
state.remove_paired_client(client_uuid)
self.driver.async_persist() self.driver.async_persist()
self.driver.async_update_advertisement() self.driver.async_update_advertisement()
self._async_show_setup_message() self._async_show_setup_message()

View File

@ -446,15 +446,25 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
return await self.async_step_advanced() return await self.async_step_advanced()
entity_filter = self.hk_options.get(CONF_FILTER, {}) entity_filter = self.hk_options.get(CONF_FILTER, {})
entities = entity_filter.get(CONF_INCLUDE_ENTITIES, [])
all_supported_entities = _async_get_matching_entities( all_supported_entities = _async_get_matching_entities(
self.hass, self.hass,
domains=self.hk_options[CONF_DOMAINS], domains=self.hk_options[CONF_DOMAINS],
) )
data_schema = {} data_schema = {}
entity_schema = vol.In # Strip out entities that no longer exist to prevent error in the UI
entities = entity_filter.get(CONF_INCLUDE_ENTITIES, []) valid_entities = [
if self.hk_options[CONF_HOMEKIT_MODE] != HOMEKIT_MODE_ACCESSORY: entity_id for entity_id in entities if entity_id in all_supported_entities
]
if self.hk_options[CONF_HOMEKIT_MODE] == HOMEKIT_MODE_ACCESSORY:
# In accessory mode we can only have one
default_value = valid_entities[0] if valid_entities else None
entity_schema = vol.In
entities_schema_required = vol.Required
else:
# Bridge mode
entities_schema_required = vol.Optional
include_exclude_mode = MODE_INCLUDE include_exclude_mode = MODE_INCLUDE
if not entities: if not entities:
include_exclude_mode = MODE_EXCLUDE include_exclude_mode = MODE_EXCLUDE
@ -463,13 +473,10 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
vol.Required(CONF_INCLUDE_EXCLUDE_MODE, default=include_exclude_mode) vol.Required(CONF_INCLUDE_EXCLUDE_MODE, default=include_exclude_mode)
] = vol.In(INCLUDE_EXCLUDE_MODES) ] = vol.In(INCLUDE_EXCLUDE_MODES)
entity_schema = cv.multi_select entity_schema = cv.multi_select
default_value = valid_entities
# Strip out entities that no longer exist to prevent error in the UI
valid_entities = [
entity_id for entity_id in entities if entity_id in all_supported_entities
]
data_schema[ data_schema[
vol.Optional(CONF_ENTITIES, default=valid_entities) entities_schema_required(CONF_ENTITIES, default=default_value)
] = entity_schema(all_supported_entities) ] = entity_schema(all_supported_entities)
return self.async_show_form( return self.async_show_form(

View File

@ -22,8 +22,8 @@ from .const import (
DEFAULT_CONSIDER_HOME, DEFAULT_CONSIDER_HOME,
DEFAULT_NAME, DEFAULT_NAME,
DOMAIN, DOMAIN,
MODELS_V2, MODELS_PORT_80,
ORBI_PORT, PORT_80,
) )
from .errors import CannotLoginException from .errors import CannotLoginException
from .router import get_api from .router import get_api
@ -141,13 +141,13 @@ class NetgearFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
self._abort_if_unique_id_configured(updates=updated_data) self._abort_if_unique_id_configured(updates=updated_data)
updated_data[CONF_PORT] = DEFAULT_PORT updated_data[CONF_PORT] = DEFAULT_PORT
for model in MODELS_V2: for model in MODELS_PORT_80:
if discovery_info.upnp.get(ssdp.ATTR_UPNP_MODEL_NUMBER, "").startswith( if discovery_info.upnp.get(ssdp.ATTR_UPNP_MODEL_NUMBER, "").startswith(
model model
) or discovery_info.upnp.get(ssdp.ATTR_UPNP_MODEL_NAME, "").startswith( ) or discovery_info.upnp.get(ssdp.ATTR_UPNP_MODEL_NAME, "").startswith(
model model
): ):
updated_data[CONF_PORT] = ORBI_PORT updated_data[CONF_PORT] = PORT_80
self.placeholders.update(updated_data) self.placeholders.update(updated_data)
self.discovered = True self.discovered = True

View File

@ -10,8 +10,8 @@ CONF_CONSIDER_HOME = "consider_home"
DEFAULT_CONSIDER_HOME = timedelta(seconds=180) DEFAULT_CONSIDER_HOME = timedelta(seconds=180)
DEFAULT_NAME = "Netgear router" DEFAULT_NAME = "Netgear router"
# update method V2 models # models using port 80 instead of 5000
MODELS_V2 = [ MODELS_PORT_80 = [
"Orbi", "Orbi",
"RBK", "RBK",
"RBR", "RBR",
@ -29,7 +29,25 @@ MODELS_V2 = [
"SXR", "SXR",
"SXS", "SXS",
] ]
ORBI_PORT = 80 PORT_80 = 80
# update method V2 models
MODELS_V2 = [
"Orbi",
"RBK",
"RBR",
"RBS",
"RBW",
"LBK",
"LBR",
"CBK",
"CBR",
"SRC",
"SRK",
"SRS",
"SXK",
"SXR",
"SXS",
]
# Icons # Icons
DEVICE_ICONS = { DEVICE_ICONS = {

View File

@ -149,6 +149,14 @@ class NetgearRouter:
if self.model.startswith(model): if self.model.startswith(model):
self.method_version = 2 self.method_version = 2
if self.method_version == 2:
if not self._api.get_attached_devices_2():
_LOGGER.error(
"Netgear Model '%s' in MODELS_V2 list, but failed to get attached devices using V2",
self.model,
)
self.method_version = 1
async def async_setup(self) -> None: async def async_setup(self) -> None:
"""Set up a Netgear router.""" """Set up a Netgear router."""
await self.hass.async_add_executor_job(self._setup) await self.hass.async_add_executor_job(self._setup)

View File

@ -3,8 +3,8 @@
"name": "Pentair ScreenLogic", "name": "Pentair ScreenLogic",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/screenlogic", "documentation": "https://www.home-assistant.io/integrations/screenlogic",
"requirements": ["screenlogicpy==0.5.3"], "requirements": ["screenlogicpy==0.5.4"],
"codeowners": ["@dieselrabbit"], "codeowners": ["@dieselrabbit", "@bdraco"],
"dhcp": [ "dhcp": [
{ {
"hostname": "pentair: *", "hostname": "pentair: *",

View File

@ -3,7 +3,7 @@
"name": "Sonos", "name": "Sonos",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/sonos", "documentation": "https://www.home-assistant.io/integrations/sonos",
"requirements": ["soco==0.25.2"], "requirements": ["soco==0.25.3"],
"dependencies": ["ssdp"], "dependencies": ["ssdp"],
"after_dependencies": ["plex", "zeroconf"], "after_dependencies": ["plex", "zeroconf"],
"zeroconf": ["_sonos._tcp.local."], "zeroconf": ["_sonos._tcp.local."],

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.13.0"], "requirements": ["PySwitchbot==0.13.2"],
"config_flow": true, "config_flow": true,
"codeowners": ["@danielhiversen", "@RenierM26"], "codeowners": ["@danielhiversen", "@RenierM26"],
"iot_class": "local_polling" "iot_class": "local_polling"

View File

@ -113,6 +113,7 @@ class SwitchBotBotEntity(SwitchbotEntity, SwitchEntity, RestoreEntity):
super().__init__(coordinator, idx, mac, name) super().__init__(coordinator, idx, mac, name)
self._attr_unique_id = idx self._attr_unique_id = idx
self._device = device self._device = device
self._attr_is_on = False
async def async_added_to_hass(self) -> None: async def async_added_to_hass(self) -> None:
"""Run when entity about to be added.""" """Run when entity about to be added."""
@ -132,6 +133,7 @@ class SwitchBotBotEntity(SwitchbotEntity, SwitchEntity, RestoreEntity):
) )
if self._last_run_success: if self._last_run_success:
self._attr_is_on = True self._attr_is_on = True
self.async_write_ha_state()
async def async_turn_off(self, **kwargs: Any) -> None: async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn device off.""" """Turn device off."""
@ -143,6 +145,7 @@ class SwitchBotBotEntity(SwitchbotEntity, SwitchEntity, RestoreEntity):
) )
if self._last_run_success: if self._last_run_success:
self._attr_is_on = False self._attr_is_on = False
self.async_write_ha_state()
@property @property
def assumed_state(self) -> bool: def assumed_state(self) -> bool:

View File

@ -142,7 +142,7 @@ class TradfriAirPurifierFan(TradfriBaseDevice, FanEntity):
preset_mode: str | None = None, preset_mode: str | None = None,
**kwargs: Any, **kwargs: Any,
) -> None: ) -> None:
"""Turn on the fan.""" """Turn on the fan. Auto-mode if no argument is given."""
if not self._device_control: if not self._device_control:
return return
@ -150,8 +150,8 @@ class TradfriAirPurifierFan(TradfriBaseDevice, FanEntity):
await self._api(self._device_control.set_mode(_from_percentage(percentage))) await self._api(self._device_control.set_mode(_from_percentage(percentage)))
return return
if preset_mode: preset_mode = preset_mode or ATTR_AUTO
await self.async_set_preset_mode(preset_mode) await self.async_set_preset_mode(preset_mode)
async def async_set_percentage(self, percentage: int) -> None: async def async_set_percentage(self, percentage: int) -> None:
"""Set the speed percentage of the fan.""" """Set the speed percentage of the fan."""

View File

@ -148,8 +148,9 @@ class TuyaClimateEntity(TuyaEntity, ClimateEntity):
): ):
self._attr_temperature_unit = TEMP_CELSIUS self._attr_temperature_unit = TEMP_CELSIUS
if any( if any(
"f" in device.status.get(dpcode, "").lower() "f" in device.status[dpcode].lower()
for dpcode in (DPCode.C_F, DPCode.TEMP_UNIT_CONVERT) for dpcode in (DPCode.C_F, DPCode.TEMP_UNIT_CONVERT)
if isinstance(device.status.get(dpcode), str)
): ):
self._attr_temperature_unit = TEMP_FAHRENHEIT self._attr_temperature_unit = TEMP_FAHRENHEIT

View File

@ -30,6 +30,28 @@ from .const import DOMAIN, TUYA_DISCOVERY_NEW, DPCode, WorkMode
from .util import remap_value from .util import remap_value
@dataclass
class ColorTypeData:
"""Color Type Data."""
h_type: IntegerTypeData
s_type: IntegerTypeData
v_type: IntegerTypeData
DEFAULT_COLOR_TYPE_DATA = ColorTypeData(
h_type=IntegerTypeData(min=1, scale=0, max=360, step=1),
s_type=IntegerTypeData(min=1, scale=0, max=255, step=1),
v_type=IntegerTypeData(min=1, scale=0, max=255, step=1),
)
DEFAULT_COLOR_TYPE_DATA_V2 = ColorTypeData(
h_type=IntegerTypeData(min=1, scale=0, max=360, step=1),
s_type=IntegerTypeData(min=1, scale=0, max=1000, step=1),
v_type=IntegerTypeData(min=1, scale=0, max=1000, step=1),
)
@dataclass @dataclass
class TuyaLightEntityDescription(LightEntityDescription): class TuyaLightEntityDescription(LightEntityDescription):
"""Describe an Tuya light entity.""" """Describe an Tuya light entity."""
@ -40,6 +62,7 @@ class TuyaLightEntityDescription(LightEntityDescription):
color_data: DPCode | tuple[DPCode, ...] | None = None color_data: DPCode | tuple[DPCode, ...] | None = None
color_mode: DPCode | None = None color_mode: DPCode | None = None
color_temp: DPCode | tuple[DPCode, ...] | None = None color_temp: DPCode | tuple[DPCode, ...] | None = None
default_color_type: ColorTypeData = DEFAULT_COLOR_TYPE_DATA
LIGHTS: dict[str, tuple[TuyaLightEntityDescription, ...]] = { LIGHTS: dict[str, tuple[TuyaLightEntityDescription, ...]] = {
@ -63,6 +86,7 @@ LIGHTS: dict[str, tuple[TuyaLightEntityDescription, ...]] = {
brightness=DPCode.BRIGHT_VALUE, brightness=DPCode.BRIGHT_VALUE,
color_temp=DPCode.TEMP_VALUE, color_temp=DPCode.TEMP_VALUE,
color_data=DPCode.COLOUR_DATA, color_data=DPCode.COLOUR_DATA,
default_color_type=DEFAULT_COLOR_TYPE_DATA_V2,
), ),
), ),
# Light # Light
@ -242,28 +266,6 @@ LIGHTS["cz"] = LIGHTS["kg"]
LIGHTS["pc"] = LIGHTS["kg"] LIGHTS["pc"] = LIGHTS["kg"]
@dataclass
class ColorTypeData:
"""Color Type Data."""
h_type: IntegerTypeData
s_type: IntegerTypeData
v_type: IntegerTypeData
DEFAULT_COLOR_TYPE_DATA = ColorTypeData(
h_type=IntegerTypeData(min=1, scale=0, max=360, step=1),
s_type=IntegerTypeData(min=1, scale=0, max=255, step=1),
v_type=IntegerTypeData(min=1, scale=0, max=255, step=1),
)
DEFAULT_COLOR_TYPE_DATA_V2 = ColorTypeData(
h_type=IntegerTypeData(min=1, scale=0, max=360, step=1),
s_type=IntegerTypeData(min=1, scale=0, max=1000, step=1),
v_type=IntegerTypeData(min=1, scale=0, max=1000, step=1),
)
@dataclass @dataclass
class ColorData: class ColorData:
"""Color Data.""" """Color Data."""
@ -443,7 +445,7 @@ class TuyaLightEntity(TuyaEntity, LightEntity):
) )
else: else:
# If no type is found, use a default one # If no type is found, use a default one
self._color_data_type = DEFAULT_COLOR_TYPE_DATA self._color_data_type = self.entity_description.default_color_type
if self._color_data_dpcode == DPCode.COLOUR_DATA_V2 or ( if self._color_data_dpcode == DPCode.COLOUR_DATA_V2 or (
self._brightness_type and self._brightness_type.max > 255 self._brightness_type and self._brightness_type.max > 255
): ):

View File

@ -2,7 +2,7 @@
"domain": "waze_travel_time", "domain": "waze_travel_time",
"name": "Waze Travel Time", "name": "Waze Travel Time",
"documentation": "https://www.home-assistant.io/integrations/waze_travel_time", "documentation": "https://www.home-assistant.io/integrations/waze_travel_time",
"requirements": ["WazeRouteCalculator==0.13"], "requirements": ["WazeRouteCalculator==0.14"],
"codeowners": [], "codeowners": [],
"config_flow": true, "config_flow": true,
"iot_class": "cloud_polling" "iot_class": "cloud_polling"

View File

@ -7,6 +7,7 @@ from homeassistant.components.number import NumberEntity, NumberEntityDescriptio
from homeassistant.components.number.const import DOMAIN as PLATFORM_DOMAIN from homeassistant.components.number.const import DOMAIN as PLATFORM_DOMAIN
from homeassistant.const import DEGREE, ENTITY_CATEGORY_CONFIG, TIME_MINUTES from homeassistant.const import DEGREE, ENTITY_CATEGORY_CONFIG, TIME_MINUTES
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers import entity_registry as er
from .const import ( from .const import (
CONF_DEVICE, CONF_DEVICE,
@ -251,7 +252,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
for feature, description in NUMBER_TYPES.items(): for feature, description in NUMBER_TYPES.items():
if feature == FEATURE_SET_LED_BRIGHTNESS and model != MODEL_FAN_ZA5: if feature == FEATURE_SET_LED_BRIGHTNESS and model != MODEL_FAN_ZA5:
# Delete LED bightness entity created by mistake if it exists # Delete LED bightness entity created by mistake if it exists
entity_reg = hass.helpers.entity_registry.async_get() entity_reg = er.async_get(hass)
entity_id = entity_reg.async_get_entity_id( entity_id = entity_reg.async_get_entity_id(
PLATFORM_DOMAIN, DOMAIN, f"{description.key}_{config_entry.unique_id}" PLATFORM_DOMAIN, DOMAIN, f"{description.key}_{config_entry.unique_id}"
) )

View File

@ -14,7 +14,14 @@ from zwave_js_server.util.command_class.meter import get_meter_type
from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_ENTITY_ID, CONF_TYPE from homeassistant.const import (
ATTR_DEVICE_ID,
ATTR_DOMAIN,
CONF_DEVICE_ID,
CONF_DOMAIN,
CONF_ENTITY_ID,
CONF_TYPE,
)
from homeassistant.core import Context, HomeAssistant from homeassistant.core import Context, HomeAssistant
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry from homeassistant.helpers import entity_registry
@ -227,7 +234,22 @@ async def async_call_action_from_config(
if action_type not in ACTION_TYPES: if action_type not in ACTION_TYPES:
raise HomeAssistantError(f"Unhandled action type {action_type}") raise HomeAssistantError(f"Unhandled action type {action_type}")
service_data = {k: v for k, v in config.items() if v not in (None, "")} # Don't include domain, subtype or any null/empty values in the service call
service_data = {
k: v
for k, v in config.items()
if k not in (ATTR_DOMAIN, CONF_SUBTYPE) and v not in (None, "")
}
# Entity services (including refresh value which is a fake entity service) expects
# just an entity ID
if action_type in (
SERVICE_REFRESH_VALUE,
SERVICE_SET_LOCK_USERCODE,
SERVICE_CLEAR_LOCK_USERCODE,
SERVICE_RESET_METER,
):
service_data.pop(ATTR_DEVICE_ID)
await hass.services.async_call( await hass.services.async_call(
DOMAIN, service, service_data, blocking=True, context=context DOMAIN, service, service_data, blocking=True, context=context
) )
@ -283,7 +305,10 @@ async def async_get_action_capabilities(
"extra_fields": vol.Schema( "extra_fields": vol.Schema(
{ {
vol.Required(ATTR_COMMAND_CLASS): vol.In( vol.Required(ATTR_COMMAND_CLASS): vol.In(
{cc.value: cc.name for cc in CommandClass} {
CommandClass(cc.id).value: cc.name
for cc in sorted(node.command_classes, key=lambda cc: cc.name) # type: ignore[no-any-return]
}
), ),
vol.Required(ATTR_PROPERTY): cv.string, vol.Required(ATTR_PROPERTY): cv.string,
vol.Optional(ATTR_PROPERTY_KEY): cv.string, vol.Optional(ATTR_PROPERTY_KEY): cv.string,

View File

@ -7,7 +7,7 @@ from homeassistant.backports.enum import StrEnum
MAJOR_VERSION: Final = 2021 MAJOR_VERSION: Final = 2021
MINOR_VERSION: Final = 12 MINOR_VERSION: Final = 12
PATCH_VERSION: Final = "8" PATCH_VERSION: Final = "9"
__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, 8, 0) REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0)

View File

@ -15,8 +15,8 @@ certifi>=2021.5.30
ciso8601==2.2.0 ciso8601==2.2.0
cryptography==35.0.0 cryptography==35.0.0
emoji==1.5.0 emoji==1.5.0
hass-nabucasa==0.50.0 hass-nabucasa==0.51.0
home-assistant-frontend==20211229.0 home-assistant-frontend==20211229.1
httpx==0.21.0 httpx==0.21.0
ifaddr==0.1.7 ifaddr==0.1.7
jinja2==3.0.3 jinja2==3.0.3

View File

@ -46,7 +46,7 @@ PyRMVtransport==0.3.3
PySocks==1.7.1 PySocks==1.7.1
# homeassistant.components.switchbot # homeassistant.components.switchbot
# PySwitchbot==0.13.0 # PySwitchbot==0.13.2
# homeassistant.components.transport_nsw # homeassistant.components.transport_nsw
PyTransportNSW==0.1.1 PyTransportNSW==0.1.1
@ -83,7 +83,7 @@ TwitterAPI==2.7.5
WSDiscovery==2.0.0 WSDiscovery==2.0.0
# homeassistant.components.waze_travel_time # homeassistant.components.waze_travel_time
WazeRouteCalculator==0.13 WazeRouteCalculator==0.14
# homeassistant.components.abode # homeassistant.components.abode
abodepy==1.2.0 abodepy==1.2.0
@ -177,7 +177,7 @@ aiogithubapi==21.11.0
aioguardian==2021.11.0 aioguardian==2021.11.0
# homeassistant.components.harmony # homeassistant.components.harmony
aioharmony==0.2.8 aioharmony==0.2.9
# homeassistant.components.homekit_controller # homeassistant.components.homekit_controller
aiohomekit==0.6.4 aiohomekit==0.6.4
@ -659,7 +659,7 @@ fjaraskupan==1.0.2
flipr-api==1.4.1 flipr-api==1.4.1
# homeassistant.components.flux_led # homeassistant.components.flux_led
flux_led==0.27.32 flux_led==0.27.45
# homeassistant.components.homekit # homeassistant.components.homekit
fnvhash==0.1.0 fnvhash==0.1.0
@ -787,7 +787,7 @@ habitipy==0.2.0
hangups==0.4.14 hangups==0.4.14
# homeassistant.components.cloud # homeassistant.components.cloud
hass-nabucasa==0.50.0 hass-nabucasa==0.51.0
# homeassistant.components.splunk # homeassistant.components.splunk
hass_splunk==0.1.1 hass_splunk==0.1.1
@ -820,7 +820,7 @@ hole==0.7.0
holidays==0.11.3.1 holidays==0.11.3.1
# homeassistant.components.frontend # homeassistant.components.frontend
home-assistant-frontend==20211229.0 home-assistant-frontend==20211229.1
# homeassistant.components.zwave # homeassistant.components.zwave
homeassistant-pyozw==0.1.10 homeassistant-pyozw==0.1.10
@ -1397,7 +1397,7 @@ pycfdns==1.2.2
pychannels==1.0.0 pychannels==1.0.0
# homeassistant.components.cast # homeassistant.components.cast
pychromecast==10.2.2 pychromecast==10.2.3
# homeassistant.components.pocketcasts # homeassistant.components.pocketcasts
pycketcasts==1.0.0 pycketcasts==1.0.0
@ -2113,7 +2113,7 @@ scapy==2.4.5
schiene==0.23 schiene==0.23
# homeassistant.components.screenlogic # homeassistant.components.screenlogic
screenlogicpy==0.5.3 screenlogicpy==0.5.4
# homeassistant.components.scsgate # homeassistant.components.scsgate
scsgate==0.1.0 scsgate==0.1.0
@ -2185,7 +2185,7 @@ smhi-pkg==1.0.15
snapcast==2.1.3 snapcast==2.1.3
# homeassistant.components.sonos # homeassistant.components.sonos
soco==0.25.2 soco==0.25.3
# homeassistant.components.solaredge_local # homeassistant.components.solaredge_local
solaredge-local==0.2.0 solaredge-local==0.2.0
@ -2466,7 +2466,7 @@ xs1-api-client==3.0.0
yalesmartalarmclient==0.3.4 yalesmartalarmclient==0.3.4
# homeassistant.components.august # homeassistant.components.august
yalexs==1.1.13 yalexs==1.1.17
# homeassistant.components.yeelight # homeassistant.components.yeelight
yeelight==0.7.8 yeelight==0.7.8

View File

@ -24,7 +24,7 @@ PyQRCode==1.2.1
PyRMVtransport==0.3.3 PyRMVtransport==0.3.3
# homeassistant.components.switchbot # homeassistant.components.switchbot
# PySwitchbot==0.13.0 # PySwitchbot==0.13.2
# homeassistant.components.transport_nsw # homeassistant.components.transport_nsw
PyTransportNSW==0.1.1 PyTransportNSW==0.1.1
@ -45,7 +45,7 @@ RtmAPI==0.7.2
WSDiscovery==2.0.0 WSDiscovery==2.0.0
# homeassistant.components.waze_travel_time # homeassistant.components.waze_travel_time
WazeRouteCalculator==0.13 WazeRouteCalculator==0.14
# homeassistant.components.abode # homeassistant.components.abode
abodepy==1.2.0 abodepy==1.2.0
@ -121,7 +121,7 @@ aioflo==2021.11.0
aioguardian==2021.11.0 aioguardian==2021.11.0
# homeassistant.components.harmony # homeassistant.components.harmony
aioharmony==0.2.8 aioharmony==0.2.9
# homeassistant.components.homekit_controller # homeassistant.components.homekit_controller
aiohomekit==0.6.4 aiohomekit==0.6.4
@ -399,7 +399,7 @@ fjaraskupan==1.0.2
flipr-api==1.4.1 flipr-api==1.4.1
# homeassistant.components.flux_led # homeassistant.components.flux_led
flux_led==0.27.32 flux_led==0.27.45
# homeassistant.components.homekit # homeassistant.components.homekit
fnvhash==0.1.0 fnvhash==0.1.0
@ -494,7 +494,7 @@ habitipy==0.2.0
hangups==0.4.14 hangups==0.4.14
# homeassistant.components.cloud # homeassistant.components.cloud
hass-nabucasa==0.50.0 hass-nabucasa==0.51.0
# homeassistant.components.tasmota # homeassistant.components.tasmota
hatasmota==0.3.1 hatasmota==0.3.1
@ -515,7 +515,7 @@ hole==0.7.0
holidays==0.11.3.1 holidays==0.11.3.1
# homeassistant.components.frontend # homeassistant.components.frontend
home-assistant-frontend==20211229.0 home-assistant-frontend==20211229.1
# homeassistant.components.zwave # homeassistant.components.zwave
homeassistant-pyozw==0.1.10 homeassistant-pyozw==0.1.10
@ -850,7 +850,7 @@ pybotvac==0.0.22
pycfdns==1.2.2 pycfdns==1.2.2
# homeassistant.components.cast # homeassistant.components.cast
pychromecast==10.2.2 pychromecast==10.2.3
# homeassistant.components.climacell # homeassistant.components.climacell
pyclimacell==0.18.2 pyclimacell==0.18.2
@ -1257,7 +1257,7 @@ samsungtvws==1.6.0
scapy==2.4.5 scapy==2.4.5
# homeassistant.components.screenlogic # homeassistant.components.screenlogic
screenlogicpy==0.5.3 screenlogicpy==0.5.4
# homeassistant.components.emulated_kasa # homeassistant.components.emulated_kasa
# homeassistant.components.sense # homeassistant.components.sense
@ -1291,7 +1291,7 @@ smarthab==0.21
smhi-pkg==1.0.15 smhi-pkg==1.0.15
# homeassistant.components.sonos # homeassistant.components.sonos
soco==0.25.2 soco==0.25.3
# homeassistant.components.solaredge # homeassistant.components.solaredge
solaredge==0.0.2 solaredge==0.0.2
@ -1464,7 +1464,7 @@ xmltodict==0.12.0
yalesmartalarmclient==0.3.4 yalesmartalarmclient==0.3.4
# homeassistant.components.august # homeassistant.components.august
yalexs==1.1.13 yalexs==1.1.17
# homeassistant.components.yeelight # homeassistant.components.yeelight
yeelight==0.7.8 yeelight==0.7.8

View File

@ -2,8 +2,6 @@
import json import json
import os import os
import time import time
# from unittest.mock import AsyncMock
from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch
from yalexs.activity import ( from yalexs.activity import (
@ -207,6 +205,8 @@ async def _mock_setup_august_with_api_side_effects(hass, api_call_side_effects,
side_effect=api_call_side_effects["unlock_return_activities"] side_effect=api_call_side_effects["unlock_return_activities"]
) )
api_instance.async_unlock_async = AsyncMock()
api_instance.async_lock_async = AsyncMock()
api_instance.async_get_user = AsyncMock(return_value={"UserID": "abc"}) api_instance.async_get_user = AsyncMock(return_value={"UserID": "abc"})
return await _mock_setup_august(hass, api_instance, pubnub) return await _mock_setup_august(hass, api_instance, pubnub)

View File

@ -1,5 +1,6 @@
"""The binary_sensor tests for the august platform.""" """The binary_sensor tests for the august platform."""
import datetime import datetime
import time
from unittest.mock import Mock, patch from unittest.mock import Mock, patch
from yalexs.pubnub_async import AugustPubNub from yalexs.pubnub_async import AugustPubNub
@ -26,6 +27,10 @@ from tests.components.august.mocks import (
) )
def _timetoken():
return str(time.time_ns())[:-2]
async def test_doorsense(hass): async def test_doorsense(hass):
"""Test creation of a lock with doorsense and bridge.""" """Test creation of a lock with doorsense and bridge."""
lock_one = await _mock_lock_from_fixture( lock_one = await _mock_lock_from_fixture(
@ -85,6 +90,10 @@ async def test_create_doorbell(hass):
"binary_sensor.k98gidt45gul_name_motion" "binary_sensor.k98gidt45gul_name_motion"
) )
assert binary_sensor_k98gidt45gul_name_motion.state == STATE_OFF assert binary_sensor_k98gidt45gul_name_motion.state == STATE_OFF
binary_sensor_k98gidt45gul_name_image_capture = hass.states.get(
"binary_sensor.k98gidt45gul_name_image_capture"
)
assert binary_sensor_k98gidt45gul_name_image_capture.state == STATE_OFF
binary_sensor_k98gidt45gul_name_online = hass.states.get( binary_sensor_k98gidt45gul_name_online = hass.states.get(
"binary_sensor.k98gidt45gul_name_online" "binary_sensor.k98gidt45gul_name_online"
) )
@ -97,6 +106,10 @@ async def test_create_doorbell(hass):
"binary_sensor.k98gidt45gul_name_motion" "binary_sensor.k98gidt45gul_name_motion"
) )
assert binary_sensor_k98gidt45gul_name_motion.state == STATE_OFF assert binary_sensor_k98gidt45gul_name_motion.state == STATE_OFF
binary_sensor_k98gidt45gul_name_image_capture = hass.states.get(
"binary_sensor.k98gidt45gul_name_image_capture"
)
assert binary_sensor_k98gidt45gul_name_image_capture.state == STATE_OFF
async def test_create_doorbell_offline(hass): async def test_create_doorbell_offline(hass):
@ -171,7 +184,7 @@ async def test_doorbell_update_via_pubnub(hass):
pubnub, pubnub,
Mock( Mock(
channel=doorbell_one.pubsub_channel, channel=doorbell_one.pubsub_channel,
timetoken=dt_util.utcnow().timestamp() * 10000000, timetoken=_timetoken(),
message={ message={
"status": "imagecapture", "status": "imagecapture",
"data": { "data": {
@ -186,10 +199,46 @@ async def test_doorbell_update_via_pubnub(hass):
await hass.async_block_till_done() await hass.async_block_till_done()
binary_sensor_k98gidt45gul_name_image_capture = hass.states.get(
"binary_sensor.k98gidt45gul_name_image_capture"
)
assert binary_sensor_k98gidt45gul_name_image_capture.state == STATE_ON
pubnub.message(
pubnub,
Mock(
channel=doorbell_one.pubsub_channel,
timetoken=_timetoken(),
message={
"status": "doorbell_motion_detected",
"data": {
"event": "doorbell_motion_detected",
"image": {
"height": 640,
"width": 480,
"format": "jpg",
"created_at": "2021-03-16T02:36:26.886Z",
"bytes": 14061,
"secure_url": "https://dyu7azbnaoi74.cloudfront.net/images/1f8.jpeg",
"url": "https://dyu7azbnaoi74.cloudfront.net/images/1f8.jpeg",
"etag": "09e839331c4ea59eef28081f2caa0e90",
},
"doorbellName": "Front Door",
"callID": None,
"origin": "mars-api",
"mutableContent": True,
},
},
),
)
await hass.async_block_till_done()
binary_sensor_k98gidt45gul_name_motion = hass.states.get( binary_sensor_k98gidt45gul_name_motion = hass.states.get(
"binary_sensor.k98gidt45gul_name_motion" "binary_sensor.k98gidt45gul_name_motion"
) )
assert binary_sensor_k98gidt45gul_name_motion.state == STATE_ON assert binary_sensor_k98gidt45gul_name_motion.state == STATE_ON
binary_sensor_k98gidt45gul_name_ding = hass.states.get( binary_sensor_k98gidt45gul_name_ding = hass.states.get(
"binary_sensor.k98gidt45gul_name_ding" "binary_sensor.k98gidt45gul_name_ding"
) )
@ -204,16 +253,16 @@ async def test_doorbell_update_via_pubnub(hass):
async_fire_time_changed(hass, new_time) async_fire_time_changed(hass, new_time)
await hass.async_block_till_done() await hass.async_block_till_done()
binary_sensor_k98gidt45gul_name_motion = hass.states.get( binary_sensor_k98gidt45gul_name_image_capture = hass.states.get(
"binary_sensor.k98gidt45gul_name_motion" "binary_sensor.k98gidt45gul_name_image_capture"
) )
assert binary_sensor_k98gidt45gul_name_motion.state == STATE_OFF assert binary_sensor_k98gidt45gul_name_image_capture.state == STATE_OFF
pubnub.message( pubnub.message(
pubnub, pubnub,
Mock( Mock(
channel=doorbell_one.pubsub_channel, channel=doorbell_one.pubsub_channel,
timetoken=dt_util.utcnow().timestamp() * 10000000, timetoken=_timetoken(),
message={ message={
"status": "buttonpush", "status": "buttonpush",
}, },
@ -274,7 +323,7 @@ async def test_door_sense_update_via_pubnub(hass):
pubnub, pubnub,
Mock( Mock(
channel=lock_one.pubsub_channel, channel=lock_one.pubsub_channel,
timetoken=dt_util.utcnow().timestamp() * 10000000, timetoken=_timetoken(),
message={"status": "kAugLockState_Unlocking", "doorState": "closed"}, message={"status": "kAugLockState_Unlocking", "doorState": "closed"},
), ),
) )
@ -289,11 +338,10 @@ async def test_door_sense_update_via_pubnub(hass):
pubnub, pubnub,
Mock( Mock(
channel=lock_one.pubsub_channel, channel=lock_one.pubsub_channel,
timetoken=dt_util.utcnow().timestamp() * 10000000, timetoken=_timetoken(),
message={"status": "kAugLockState_Locking", "doorState": "open"}, message={"status": "kAugLockState_Locking", "doorState": "open"},
), ),
) )
await hass.async_block_till_done() await hass.async_block_till_done()
binary_sensor_online_with_doorsense_name = hass.states.get( binary_sensor_online_with_doorsense_name = hass.states.get(
"binary_sensor.online_with_doorsense_name_open" "binary_sensor.online_with_doorsense_name_open"
@ -327,7 +375,7 @@ async def test_door_sense_update_via_pubnub(hass):
pubnub, pubnub,
Mock( Mock(
channel=lock_one.pubsub_channel, channel=lock_one.pubsub_channel,
timetoken=dt_util.utcnow().timestamp() * 10000000, timetoken=_timetoken(),
message={"status": "kAugLockState_Unlocking", "doorState": "open"}, message={"status": "kAugLockState_Unlocking", "doorState": "open"},
), ),
) )

View File

@ -154,6 +154,86 @@ async def test_one_lock_operation(hass):
) )
async def test_one_lock_operation_pubnub_connected(hass):
"""Test lock and unlock operations are async when pubnub is connected."""
lock_one = await _mock_doorsense_enabled_august_lock_detail(hass)
assert lock_one.pubsub_channel == "pubsub"
pubnub = AugustPubNub()
await _create_august_with_devices(hass, [lock_one], pubnub=pubnub)
pubnub.connected = True
lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name")
assert lock_online_with_doorsense_name.state == STATE_LOCKED
assert lock_online_with_doorsense_name.attributes.get("battery_level") == 92
assert (
lock_online_with_doorsense_name.attributes.get("friendly_name")
== "online_with_doorsense Name"
)
data = {ATTR_ENTITY_ID: "lock.online_with_doorsense_name"}
assert await hass.services.async_call(
LOCK_DOMAIN, SERVICE_UNLOCK, data, blocking=True
)
await hass.async_block_till_done()
pubnub.message(
pubnub,
Mock(
channel=lock_one.pubsub_channel,
timetoken=(dt_util.utcnow().timestamp() + 1) * 10000000,
message={
"status": "kAugLockState_Unlocked",
},
),
)
await hass.async_block_till_done()
await hass.async_block_till_done()
lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name")
assert lock_online_with_doorsense_name.state == STATE_UNLOCKED
assert lock_online_with_doorsense_name.attributes.get("battery_level") == 92
assert (
lock_online_with_doorsense_name.attributes.get("friendly_name")
== "online_with_doorsense Name"
)
assert await hass.services.async_call(
LOCK_DOMAIN, SERVICE_LOCK, data, blocking=True
)
await hass.async_block_till_done()
pubnub.message(
pubnub,
Mock(
channel=lock_one.pubsub_channel,
timetoken=(dt_util.utcnow().timestamp() + 2) * 10000000,
message={
"status": "kAugLockState_Locked",
},
),
)
await hass.async_block_till_done()
await hass.async_block_till_done()
lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name")
assert lock_online_with_doorsense_name.state == STATE_LOCKED
# No activity means it will be unavailable until the activity feed has data
entity_registry = er.async_get(hass)
lock_operator_sensor = entity_registry.async_get(
"sensor.online_with_doorsense_name_operator"
)
assert lock_operator_sensor
assert (
hass.states.get("sensor.online_with_doorsense_name_operator").state
== STATE_UNKNOWN
)
async def test_lock_jammed(hass): async def test_lock_jammed(hass):
"""Test lock gets jammed on unlock.""" """Test lock gets jammed on unlock."""
@ -273,6 +353,7 @@ async def test_lock_update_via_pubnub(hass):
config_entry = await _create_august_with_devices( config_entry = await _create_august_with_devices(
hass, [lock_one], activities=activities, pubnub=pubnub hass, [lock_one], activities=activities, pubnub=pubnub
) )
pubnub.connected = True
lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name") lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name")

View File

@ -15,6 +15,7 @@ from homeassistant.components.alexa.entities import LightCapabilities
from homeassistant.components.cloud.const import DOMAIN, RequireRelink from homeassistant.components.cloud.const import DOMAIN, RequireRelink
from homeassistant.components.google_assistant.helpers import GoogleEntity from homeassistant.components.google_assistant.helpers import GoogleEntity
from homeassistant.core import State from homeassistant.core import State
from homeassistant.util.location import LocationInfo
from . import mock_cloud, mock_cloud_prefs from . import mock_cloud, mock_cloud_prefs
@ -203,16 +204,60 @@ async def test_logout_view_unknown_error(hass, cloud_client):
assert req.status == HTTPStatus.BAD_GATEWAY assert req.status == HTTPStatus.BAD_GATEWAY
async def test_register_view(mock_cognito, cloud_client): async def test_register_view_no_location(mock_cognito, cloud_client):
"""Test logging out.""" """Test register without location."""
req = await cloud_client.post( with patch(
"/api/cloud/register", json={"email": "hello@bla.com", "password": "falcon42"} "homeassistant.components.cloud.http_api.async_detect_location_info",
) return_value=None,
):
req = await cloud_client.post(
"/api/cloud/register",
json={"email": "hello@bla.com", "password": "falcon42"},
)
assert req.status == HTTPStatus.OK assert req.status == HTTPStatus.OK
assert len(mock_cognito.register.mock_calls) == 1 assert len(mock_cognito.register.mock_calls) == 1
result_email, result_pass = mock_cognito.register.mock_calls[0][1] call = mock_cognito.register.mock_calls[0]
result_email, result_pass = call.args
assert result_email == "hello@bla.com" assert result_email == "hello@bla.com"
assert result_pass == "falcon42" assert result_pass == "falcon42"
assert call.kwargs["client_metadata"] is None
async def test_register_view_with_location(mock_cognito, cloud_client):
"""Test register with location."""
with patch(
"homeassistant.components.cloud.http_api.async_detect_location_info",
return_value=LocationInfo(
**{
"country_code": "XX",
"zip_code": "12345",
"region_code": "GH",
"ip": "1.2.3.4",
"city": "Gotham",
"region_name": "Gotham",
"time_zone": "Earth/Gotham",
"currency": "XXX",
"latitude": "12.34567",
"longitude": "12.34567",
"use_metric": True,
}
),
):
req = await cloud_client.post(
"/api/cloud/register",
json={"email": "hello@bla.com", "password": "falcon42"},
)
assert req.status == HTTPStatus.OK
assert len(mock_cognito.register.mock_calls) == 1
call = mock_cognito.register.mock_calls[0]
result_email, result_pass = call.args
assert result_email == "hello@bla.com"
assert result_pass == "falcon42"
assert call.kwargs["client_metadata"] == {
"NC_COUNTRY_CODE": "XX",
"NC_REGION_CODE": "GH",
"NC_ZIP_CODE": "12345",
}
async def test_register_view_bad_data(mock_cognito, cloud_client): async def test_register_view_bad_data(mock_cognito, cloud_client):

View File

@ -7,6 +7,8 @@ from homeassistant.config_entries import SOURCE_IGNORE, SOURCE_IMPORT
from homeassistant.const import CONF_NAME, CONF_PORT from homeassistant.const import CONF_NAME, CONF_PORT
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from .util import PATH_HOMEKIT, async_init_entry
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
@ -1065,11 +1067,13 @@ async def test_options_flow_blocked_when_from_yaml(hass, mock_get_source_ip):
assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
async def test_options_flow_include_mode_basic_accessory(hass, mock_get_source_ip): @patch(f"{PATH_HOMEKIT}.async_port_is_available", return_value=True)
async def test_options_flow_include_mode_basic_accessory(
port_mock, hass, mock_get_source_ip, hk_driver, mock_async_zeroconf
):
"""Test config flow options in include mode with a single accessory.""" """Test config flow options in include mode with a single accessory."""
config_entry = _mock_config_entry_with_options_populated() config_entry = _mock_config_entry_with_options_populated()
config_entry.add_to_hass(hass) await async_init_entry(hass, config_entry)
hass.states.async_set("media_player.tv", "off") hass.states.async_set("media_player.tv", "off")
hass.states.async_set("media_player.sonos", "off") hass.states.async_set("media_player.sonos", "off")
@ -1101,7 +1105,48 @@ async def test_options_flow_include_mode_basic_accessory(hass, mock_get_source_i
assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result2["step_id"] == "include_exclude" assert result2["step_id"] == "include_exclude"
assert _get_schema_default(result2["data_schema"].schema, "entities") == [] assert _get_schema_default(result2["data_schema"].schema, "entities") is None
result3 = await hass.config_entries.options.async_configure(
result2["flow_id"],
user_input={"entities": "media_player.tv"},
)
assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert config_entry.options == {
"mode": "accessory",
"filter": {
"exclude_domains": [],
"exclude_entities": [],
"include_domains": [],
"include_entities": ["media_player.tv"],
},
}
# Now we check again to make sure the single entity is still
# preselected
result = await hass.config_entries.options.async_init(
config_entry.entry_id, context={"show_advanced_options": False}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "init"
assert result["data_schema"]({}) == {
"domains": ["media_player"],
"mode": "accessory",
}
result2 = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={"domains": ["media_player"], "mode": "accessory"},
)
assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result2["step_id"] == "include_exclude"
assert (
_get_schema_default(result2["data_schema"].schema, "entities")
== "media_player.tv"
)
result3 = await hass.config_entries.options.async_configure( result3 = await hass.config_entries.options.async_configure(
result2["flow_id"], result2["flow_id"],

View File

@ -683,6 +683,11 @@ async def test_homekit_unpair(hass, device_reg, mock_async_zeroconf):
state = homekit.driver.state state = homekit.driver.state
state.add_paired_client("client1", "any", b"1") state.add_paired_client("client1", "any", b"1")
state.add_paired_client("client2", "any", b"0")
state.add_paired_client("client3", "any", b"1")
state.add_paired_client("client4", "any", b"0")
state.add_paired_client("client5", "any", b"0")
formatted_mac = device_registry.format_mac(state.mac) formatted_mac = device_registry.format_mac(state.mac)
hk_bridge_dev = device_reg.async_get_device( hk_bridge_dev = device_reg.async_get_device(
{}, {(device_registry.CONNECTION_NETWORK_MAC, formatted_mac)} {}, {(device_registry.CONNECTION_NETWORK_MAC, formatted_mac)}

View File

@ -6,7 +6,7 @@ import pytest
from homeassistant import data_entry_flow from homeassistant import data_entry_flow
from homeassistant.components import ssdp from homeassistant.components import ssdp
from homeassistant.components.netgear.const import CONF_CONSIDER_HOME, DOMAIN, ORBI_PORT from homeassistant.components.netgear.const import CONF_CONSIDER_HOME, DOMAIN, PORT_80
from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_SSDP, SOURCE_USER from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_SSDP, SOURCE_USER
from homeassistant.const import ( from homeassistant.const import (
CONF_HOST, CONF_HOST,
@ -252,7 +252,7 @@ async def test_ssdp(hass, service):
assert result["result"].unique_id == SERIAL assert result["result"].unique_id == SERIAL
assert result["title"] == TITLE assert result["title"] == TITLE
assert result["data"].get(CONF_HOST) == HOST assert result["data"].get(CONF_HOST) == HOST
assert result["data"].get(CONF_PORT) == ORBI_PORT assert result["data"].get(CONF_PORT) == PORT_80
assert result["data"].get(CONF_SSL) == SSL assert result["data"].get(CONF_SSL) == SSL
assert result["data"].get(CONF_USERNAME) == DEFAULT_USER assert result["data"].get(CONF_USERNAME) == DEFAULT_USER
assert result["data"][CONF_PASSWORD] == PASSWORD assert result["data"][CONF_PASSWORD] == PASSWORD

View File

@ -57,7 +57,128 @@
}, },
{ "nodeId": 13, "index": 2 } { "nodeId": 13, "index": 2 }
], ],
"commandClasses": [], "commandClasses": [
{
"id": 49,
"name": "Multilevel Sensor",
"version": 5,
"isSecure": false
},
{
"id": 64,
"name": "Thermostat Mode",
"version": 2,
"isSecure": false
},
{
"id": 66,
"name": "Thermostat Operating State",
"version": 2,
"isSecure": false
},
{
"id": 67,
"name": "Thermostat Setpoint",
"version": 2,
"isSecure": false
},
{
"id": 68,
"name": "Thermostat Fan Mode",
"version": 1,
"isSecure": false
},
{
"id": 69,
"name": "Thermostat Fan State",
"version": 1,
"isSecure": false
},
{
"id": 89,
"name": "Association Group Information",
"version": 1,
"isSecure": false
},
{
"id": 90,
"name": "Device Reset Locally",
"version": 1,
"isSecure": false
},
{
"id": 94,
"name": "Z-Wave Plus Info",
"version": 2,
"isSecure": false
},
{
"id": 96,
"name": "Multi Channel",
"version": 4,
"isSecure": false
},
{
"id": 112,
"name": "Configuration",
"version": 1,
"isSecure": false
},
{
"id": 114,
"name": "Manufacturer Specific",
"version": 2,
"isSecure": false
},
{
"id": 115,
"name": "Powerlevel",
"version": 1,
"isSecure": false
},
{
"id": 122,
"name": "Firmware Update Meta Data",
"version": 3,
"isSecure": false
},
{
"id": 128,
"name": "Battery",
"version": 1,
"isSecure": false
},
{
"id": 129,
"name": "Clock",
"version": 1,
"isSecure": false
},
{
"id": 133,
"name": "Association",
"version": 2,
"isSecure": false
},
{
"id": 134,
"name": "Version",
"version": 2,
"isSecure": false
},
{
"id": 135,
"name": "Indicator",
"version": 1,
"isSecure": false
},
{
"id": 142,
"name": "Multi Channel Association",
"version": 3,
"isSecure": false
}
],
"values": [ "values": [
{ {
"commandClassName": "Manufacturer Specific", "commandClassName": "Manufacturer Specific",

View File

@ -1,4 +1,6 @@
"""The tests for Z-Wave JS device actions.""" """The tests for Z-Wave JS device actions."""
from unittest.mock import patch
import pytest import pytest
import voluptuous_serialize import voluptuous_serialize
from zwave_js_server.client import Client from zwave_js_server.client import Client
@ -14,7 +16,7 @@ from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv, device_registry from homeassistant.helpers import config_validation as cv, device_registry
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from tests.common import async_get_device_automations, async_mock_service from tests.common import async_get_device_automations
async def test_get_actions( async def test_get_actions(
@ -87,8 +89,130 @@ async def test_get_actions_meter(
assert len(filtered_actions) > 0 assert len(filtered_actions) > 0
async def test_action(hass: HomeAssistant) -> None: async def test_actions(
"""Test for turn_on and turn_off actions.""" hass: HomeAssistant,
client: Client,
climate_radio_thermostat_ct100_plus: Node,
integration: ConfigEntry,
) -> None:
"""Test actions."""
node = climate_radio_thermostat_ct100_plus
device_id = get_device_id(client, node)
dev_reg = device_registry.async_get(hass)
device = dev_reg.async_get_device({device_id})
assert device
assert await async_setup_component(
hass,
automation.DOMAIN,
{
automation.DOMAIN: [
{
"trigger": {
"platform": "event",
"event_type": "test_event_refresh_value",
},
"action": {
"domain": DOMAIN,
"type": "refresh_value",
"device_id": device.id,
"entity_id": "climate.z_wave_thermostat",
},
},
{
"trigger": {
"platform": "event",
"event_type": "test_event_ping",
},
"action": {
"domain": DOMAIN,
"type": "ping",
"device_id": device.id,
},
},
{
"trigger": {
"platform": "event",
"event_type": "test_event_set_value",
},
"action": {
"domain": DOMAIN,
"type": "set_value",
"device_id": device.id,
"command_class": 112,
"property": 1,
"value": 1,
},
},
{
"trigger": {
"platform": "event",
"event_type": "test_event_set_config_parameter",
},
"action": {
"domain": DOMAIN,
"type": "set_config_parameter",
"device_id": device.id,
"parameter": 1,
"bitmask": None,
"subtype": "2-112-0-3 (Beeper)",
"value": 1,
},
},
]
},
)
with patch("zwave_js_server.model.node.Node.async_poll_value") as mock_call:
hass.bus.async_fire("test_event_refresh_value")
await hass.async_block_till_done()
mock_call.assert_called_once()
args = mock_call.call_args_list[0][0]
assert len(args) == 1
assert args[0].value_id == "13-64-1-mode"
with patch("zwave_js_server.model.node.Node.async_ping") as mock_call:
hass.bus.async_fire("test_event_ping")
await hass.async_block_till_done()
mock_call.assert_called_once()
args = mock_call.call_args_list[0][0]
assert len(args) == 0
with patch("zwave_js_server.model.node.Node.async_set_value") as mock_call:
hass.bus.async_fire("test_event_set_value")
await hass.async_block_till_done()
mock_call.assert_called_once()
args = mock_call.call_args_list[0][0]
assert len(args) == 2
assert args[0] == "13-112-0-1"
assert args[1] == 1
with patch(
"homeassistant.components.zwave_js.services.async_set_config_parameter"
) as mock_call:
hass.bus.async_fire("test_event_set_config_parameter")
await hass.async_block_till_done()
mock_call.assert_called_once()
args = mock_call.call_args_list[0][0]
assert len(args) == 3
assert args[0].node_id == 13
assert args[1] == 1
assert args[2] == 1
async def test_lock_actions(
hass: HomeAssistant,
client: Client,
lock_schlage_be469: Node,
integration: ConfigEntry,
) -> None:
"""Test actions for locks."""
node = lock_schlage_be469
device_id = get_device_id(client, node)
dev_reg = device_registry.async_get(hass)
device = dev_reg.async_get_device({device_id})
assert device
assert await async_setup_component( assert await async_setup_component(
hass, hass,
automation.DOMAIN, automation.DOMAIN,
@ -102,7 +226,7 @@ async def test_action(hass: HomeAssistant) -> None:
"action": { "action": {
"domain": DOMAIN, "domain": DOMAIN,
"type": "clear_lock_usercode", "type": "clear_lock_usercode",
"device_id": "fake", "device_id": device.id,
"entity_id": "lock.touchscreen_deadbolt", "entity_id": "lock.touchscreen_deadbolt",
"code_slot": 1, "code_slot": 1,
}, },
@ -115,97 +239,80 @@ async def test_action(hass: HomeAssistant) -> None:
"action": { "action": {
"domain": DOMAIN, "domain": DOMAIN,
"type": "set_lock_usercode", "type": "set_lock_usercode",
"device_id": "fake", "device_id": device.id,
"entity_id": "lock.touchscreen_deadbolt", "entity_id": "lock.touchscreen_deadbolt",
"code_slot": 1, "code_slot": 1,
"usercode": "1234", "usercode": "1234",
}, },
}, },
{
"trigger": {
"platform": "event",
"event_type": "test_event_refresh_value",
},
"action": {
"domain": DOMAIN,
"type": "refresh_value",
"device_id": "fake",
"entity_id": "lock.touchscreen_deadbolt",
},
},
{
"trigger": {
"platform": "event",
"event_type": "test_event_ping",
},
"action": {
"domain": DOMAIN,
"type": "ping",
"device_id": "fake",
},
},
{
"trigger": {
"platform": "event",
"event_type": "test_event_set_value",
},
"action": {
"domain": DOMAIN,
"type": "set_value",
"device_id": "fake",
"command_class": 112,
"property": "test",
"value": 1,
},
},
{
"trigger": {
"platform": "event",
"event_type": "test_event_set_config_parameter",
},
"action": {
"domain": DOMAIN,
"type": "set_config_parameter",
"device_id": "fake",
"parameter": 3,
"bitmask": None,
"subtype": "2-112-0-3 (Beeper)",
"value": 255,
},
},
] ]
}, },
) )
clear_lock_usercode = async_mock_service(hass, "zwave_js", "clear_lock_usercode") with patch("homeassistant.components.zwave_js.lock.clear_usercode") as mock_call:
hass.bus.async_fire("test_event_clear_lock_usercode") hass.bus.async_fire("test_event_clear_lock_usercode")
await hass.async_block_till_done() await hass.async_block_till_done()
assert len(clear_lock_usercode) == 1 mock_call.assert_called_once()
args = mock_call.call_args_list[0][0]
assert len(args) == 2
assert args[0].node_id == node.node_id
assert args[1] == 1
set_lock_usercode = async_mock_service(hass, "zwave_js", "set_lock_usercode") with patch("homeassistant.components.zwave_js.lock.set_usercode") as mock_call:
hass.bus.async_fire("test_event_set_lock_usercode") hass.bus.async_fire("test_event_set_lock_usercode")
await hass.async_block_till_done() await hass.async_block_till_done()
assert len(set_lock_usercode) == 1 mock_call.assert_called_once()
args = mock_call.call_args_list[0][0]
assert len(args) == 3
assert args[0].node_id == node.node_id
assert args[1] == 1
assert args[2] == "1234"
refresh_value = async_mock_service(hass, "zwave_js", "refresh_value")
hass.bus.async_fire("test_event_refresh_value")
await hass.async_block_till_done()
assert len(refresh_value) == 1
ping = async_mock_service(hass, "zwave_js", "ping") async def test_reset_meter_action(
hass.bus.async_fire("test_event_ping") hass: HomeAssistant,
await hass.async_block_till_done() client: Client,
assert len(ping) == 1 aeon_smart_switch_6: Node,
integration: ConfigEntry,
) -> None:
"""Test reset_meter action."""
node = aeon_smart_switch_6
device_id = get_device_id(client, node)
dev_reg = device_registry.async_get(hass)
device = dev_reg.async_get_device({device_id})
assert device
set_value = async_mock_service(hass, "zwave_js", "set_value") assert await async_setup_component(
hass.bus.async_fire("test_event_set_value") hass,
await hass.async_block_till_done() automation.DOMAIN,
assert len(set_value) == 1 {
automation.DOMAIN: [
{
"trigger": {
"platform": "event",
"event_type": "test_event_reset_meter",
},
"action": {
"domain": DOMAIN,
"type": "reset_meter",
"device_id": device.id,
"entity_id": "sensor.smart_switch_6_electric_consumed_kwh",
},
},
]
},
)
set_config_parameter = async_mock_service(hass, "zwave_js", "set_config_parameter") with patch(
hass.bus.async_fire("test_event_set_config_parameter") "zwave_js_server.model.endpoint.Endpoint.async_invoke_cc_api"
await hass.async_block_till_done() ) as mock_call:
assert len(set_config_parameter) == 1 hass.bus.async_fire("test_event_reset_meter")
await hass.async_block_till_done()
mock_call.assert_called_once()
args = mock_call.call_args_list[0][0]
assert len(args) == 2
assert args[0] == CommandClass.METER
assert args[1] == "reset"
async def test_get_action_capabilities( async def test_get_action_capabilities(
@ -261,7 +368,28 @@ async def test_get_action_capabilities(
) )
assert capabilities and "extra_fields" in capabilities assert capabilities and "extra_fields" in capabilities
cc_options = [(cc.value, cc.name) for cc in CommandClass] cc_options = [
(133, "Association"),
(89, "Association Group Information"),
(128, "Battery"),
(129, "Clock"),
(112, "Configuration"),
(90, "Device Reset Locally"),
(122, "Firmware Update Meta Data"),
(135, "Indicator"),
(114, "Manufacturer Specific"),
(96, "Multi Channel"),
(142, "Multi Channel Association"),
(49, "Multilevel Sensor"),
(115, "Powerlevel"),
(68, "Thermostat Fan Mode"),
(69, "Thermostat Fan State"),
(64, "Thermostat Mode"),
(66, "Thermostat Operating State"),
(67, "Thermostat Setpoint"),
(134, "Version"),
(94, "Z-Wave Plus Info"),
]
assert voluptuous_serialize.convert( assert voluptuous_serialize.convert(
capabilities["extra_fields"], custom_serializer=cv.custom_serializer capabilities["extra_fields"], custom_serializer=cv.custom_serializer