Merge pull request #47645 from home-assistant/rc

This commit is contained in:
Paulus Schoutsen 2021-03-08 17:40:16 -08:00 committed by GitHub
commit a8844ff24a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
55 changed files with 1201 additions and 266 deletions

View File

@ -46,7 +46,7 @@ homeassistant/components/arcam_fmj/* @elupus
homeassistant/components/arduino/* @fabaff homeassistant/components/arduino/* @fabaff
homeassistant/components/arest/* @fabaff homeassistant/components/arest/* @fabaff
homeassistant/components/arris_tg2492lg/* @vanbalken homeassistant/components/arris_tg2492lg/* @vanbalken
homeassistant/components/asuswrt/* @kennedyshead homeassistant/components/asuswrt/* @kennedyshead @ollo69
homeassistant/components/atag/* @MatsNL homeassistant/components/atag/* @MatsNL
homeassistant/components/aten_pe/* @mtdcr homeassistant/components/aten_pe/* @mtdcr
homeassistant/components/atome/* @baqs homeassistant/components/atome/* @baqs

View File

@ -393,7 +393,7 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator):
), ),
ATTR_FORECAST_TEMP: self._get_temperature_day(day), ATTR_FORECAST_TEMP: self._get_temperature_day(day),
ATTR_FORECAST_TEMP_LOW: self._get_temperature_low_day(day), ATTR_FORECAST_TEMP_LOW: self._get_temperature_low_day(day),
ATTR_FORECAST_TIME: dt_util.as_utc(date), ATTR_FORECAST_TIME: dt_util.as_utc(date).isoformat(),
ATTR_FORECAST_WIND_SPEED: self._get_wind_speed_day(day), ATTR_FORECAST_WIND_SPEED: self._get_wind_speed_day(day),
ATTR_FORECAST_WIND_BEARING: self._get_wind_bearing_day(day), ATTR_FORECAST_WIND_BEARING: self._get_wind_bearing_day(day),
} }
@ -412,7 +412,7 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator):
day, hour day, hour
), ),
ATTR_FORECAST_TEMP: self._get_temperature(day, hour), ATTR_FORECAST_TEMP: self._get_temperature(day, hour),
ATTR_FORECAST_TIME: dt_util.as_utc(forecast_dt), ATTR_FORECAST_TIME: dt_util.as_utc(forecast_dt).isoformat(),
ATTR_FORECAST_WIND_SPEED: self._get_wind_speed(day, hour), ATTR_FORECAST_WIND_SPEED: self._get_wind_speed(day, hour),
ATTR_FORECAST_WIND_BEARING: self._get_wind_bearing(day, hour), ATTR_FORECAST_WIND_BEARING: self._get_wind_bearing(day, hour),
} }

View File

@ -128,7 +128,7 @@ class AsusWrtFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
conf_protocol = user_input[CONF_PROTOCOL] conf_protocol = user_input[CONF_PROTOCOL]
if conf_protocol == PROTOCOL_TELNET: if conf_protocol == PROTOCOL_TELNET:
await api.connection.disconnect() api.connection.disconnect()
return RESULT_SUCCESS return RESULT_SUCCESS
async def async_step_user(self, user_input=None): async def async_step_user(self, user_input=None):

View File

@ -4,5 +4,5 @@
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/asuswrt", "documentation": "https://www.home-assistant.io/integrations/asuswrt",
"requirements": ["aioasuswrt==1.3.1"], "requirements": ["aioasuswrt==1.3.1"],
"codeowners": ["@kennedyshead"] "codeowners": ["@kennedyshead", "@ollo69"]
} }

View File

@ -205,7 +205,7 @@ class AsusWrtRouter:
"""Close the connection.""" """Close the connection."""
if self._api is not None: if self._api is not None:
if self._protocol == PROTOCOL_TELNET: if self._protocol == PROTOCOL_TELNET:
await self._api.connection.disconnect() self._api.connection.disconnect()
self._api = None self._api = None
for func in self._on_close: for func in self._on_close:

View File

@ -102,7 +102,7 @@ class BondEntity(Entity):
async def _async_update_if_bpup_not_alive(self, *_): async def _async_update_if_bpup_not_alive(self, *_):
"""Fetch via the API if BPUP is not alive.""" """Fetch via the API if BPUP is not alive."""
if self._bpup_subs.alive and self._initialized: if self._bpup_subs.alive and self._initialized and self._available:
return return
if self._update_lock.locked(): if self._update_lock.locked():

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==20210302.5" "home-assistant-frontend==20210302.6"
], ],
"dependencies": [ "dependencies": [
"api", "api",

View File

@ -616,7 +616,7 @@ class HomeKit:
self._async_register_bridge(dev_reg) self._async_register_bridge(dev_reg)
await self._async_start(bridged_states) await self._async_start(bridged_states)
_LOGGER.debug("Driver start for %s", self._name) _LOGGER.debug("Driver start for %s", self._name)
self.hass.add_job(self.driver.start_service) await self.driver.async_start()
self.status = STATUS_RUNNING self.status = STATUS_RUNNING
@callback @callback

View File

@ -3,7 +3,7 @@
"name": "HomeKit", "name": "HomeKit",
"documentation": "https://www.home-assistant.io/integrations/homekit", "documentation": "https://www.home-assistant.io/integrations/homekit",
"requirements": [ "requirements": [
"HAP-python==3.3.2", "HAP-python==3.4.0",
"fnvhash==0.1.0", "fnvhash==0.1.0",
"PyQRCode==1.2.1", "PyQRCode==1.2.1",
"base36==0.1.1", "base36==0.1.1",

View File

@ -27,7 +27,7 @@ from homeassistant.const import (
STATE_ON, STATE_ON,
) )
from homeassistant.core import callback, split_entity_id from homeassistant.core import callback, split_entity_id
from homeassistant.helpers.event import call_later from homeassistant.helpers.event import async_call_later
from .accessories import TYPES, HomeAccessory from .accessories import TYPES, HomeAccessory
from .const import ( from .const import (
@ -134,7 +134,7 @@ class Switch(HomeAccessory):
self.async_call_service(self._domain, service, params) self.async_call_service(self._domain, service, params)
if self.activate_only: if self.activate_only:
call_later(self.hass, 1, self.reset_switch) async_call_later(self.hass, 1, self.reset_switch)
@callback @callback
def async_update_state(self, new_state): def async_update_state(self, new_state):

View File

@ -80,6 +80,13 @@ class BaseHomeKitFan(HomeKitEntity, FanEntity):
return features return features
@property
def speed_count(self):
"""Speed count for the fan."""
return round(
100 / max(1, self.service[CharacteristicsTypes.ROTATION_SPEED].minStep or 0)
)
async def async_set_direction(self, direction): async def async_set_direction(self, direction):
"""Set the direction of the fan.""" """Set the direction of the fan."""
await self.async_put_characteristics( await self.async_put_characteristics(
@ -110,7 +117,7 @@ class BaseHomeKitFan(HomeKitEntity, FanEntity):
if not self.is_on: if not self.is_on:
characteristics[self.on_characteristic] = True characteristics[self.on_characteristic] = True
if self.supported_features & SUPPORT_SET_SPEED: if percentage is not None and self.supported_features & SUPPORT_SET_SPEED:
characteristics[CharacteristicsTypes.ROTATION_SPEED] = percentage characteristics[CharacteristicsTypes.ROTATION_SPEED] = percentage
if characteristics: if characteristics:

View File

@ -1,8 +1,6 @@
"""Support for INSTEON fans via PowerLinc Modem.""" """Support for INSTEON fans via PowerLinc Modem."""
import math import math
from pyinsteon.constants import FanSpeed
from homeassistant.components.fan import ( from homeassistant.components.fan import (
DOMAIN as FAN_DOMAIN, DOMAIN as FAN_DOMAIN,
SUPPORT_SET_SPEED, SUPPORT_SET_SPEED,
@ -19,7 +17,7 @@ from .const import SIGNAL_ADD_ENTITIES
from .insteon_entity import InsteonEntity from .insteon_entity import InsteonEntity
from .utils import async_add_insteon_entities from .utils import async_add_insteon_entities
SPEED_RANGE = (1, FanSpeed.HIGH) # off is not included SPEED_RANGE = (0x00, 0xFF) # off is not included
async def async_setup_entry(hass, config_entry, async_add_entities): async def async_setup_entry(hass, config_entry, async_add_entities):
@ -52,6 +50,11 @@ class InsteonFanEntity(InsteonEntity, FanEntity):
"""Flag supported features.""" """Flag supported features."""
return SUPPORT_SET_SPEED return SUPPORT_SET_SPEED
@property
def speed_count(self) -> int:
"""Flag supported features."""
return 3
async def async_turn_on( async def async_turn_on(
self, self,
speed: str = None, speed: str = None,
@ -60,9 +63,7 @@ class InsteonFanEntity(InsteonEntity, FanEntity):
**kwargs, **kwargs,
) -> None: ) -> None:
"""Turn on the fan.""" """Turn on the fan."""
if percentage is None: await self.async_set_percentage(percentage or 67)
percentage = 50
await self.async_set_percentage(percentage)
async def async_turn_off(self, **kwargs) -> None: async def async_turn_off(self, **kwargs) -> None:
"""Turn off the fan.""" """Turn off the fan."""
@ -71,7 +72,7 @@ class InsteonFanEntity(InsteonEntity, FanEntity):
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."""
if percentage == 0: if percentage == 0:
await self._insteon_device.async_fan_off() await self.async_turn_off()
else: return
on_level = math.ceil(percentage_to_ranged_value(SPEED_RANGE, percentage)) on_level = math.ceil(percentage_to_ranged_value(SPEED_RANGE, percentage))
await self._insteon_device.async_fan_on(on_level=on_level) await self._insteon_device.async_on(group=2, on_level=on_level)

View File

@ -35,9 +35,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
hub = LitterRobotHub(self.hass, user_input) hub = LitterRobotHub(self.hass, user_input)
try: try:
await hub.login() await hub.login()
return self.async_create_entry(
title=user_input[CONF_USERNAME], data=user_input
)
except LitterRobotLoginException: except LitterRobotLoginException:
errors["base"] = "invalid_auth" errors["base"] = "invalid_auth"
except LitterRobotException: except LitterRobotException:
@ -46,6 +43,11 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
_LOGGER.exception("Unexpected exception") _LOGGER.exception("Unexpected exception")
errors["base"] = "unknown" errors["base"] = "unknown"
if not errors:
return self.async_create_entry(
title=user_input[CONF_USERNAME], data=user_input
)
return self.async_show_form( return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
) )

View File

@ -4,7 +4,7 @@ import logging
from types import MethodType from types import MethodType
from typing import Any, Optional from typing import Any, Optional
from pylitterbot import Account, Robot import pylitterbot
from pylitterbot.exceptions import LitterRobotException, LitterRobotLoginException from pylitterbot.exceptions import LitterRobotException, LitterRobotLoginException
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
@ -49,7 +49,7 @@ class LitterRobotHub:
async def login(self, load_robots: bool = False): async def login(self, load_robots: bool = False):
"""Login to Litter-Robot.""" """Login to Litter-Robot."""
self.logged_in = False self.logged_in = False
self.account = Account() self.account = pylitterbot.Account()
try: try:
await self.account.connect( await self.account.connect(
username=self._data[CONF_USERNAME], username=self._data[CONF_USERNAME],
@ -69,11 +69,11 @@ class LitterRobotHub:
class LitterRobotEntity(CoordinatorEntity): class LitterRobotEntity(CoordinatorEntity):
"""Generic Litter-Robot entity representing common data and methods.""" """Generic Litter-Robot entity representing common data and methods."""
def __init__(self, robot: Robot, entity_type: str, hub: LitterRobotHub): def __init__(self, robot: pylitterbot.Robot, entity_type: str, hub: LitterRobotHub):
"""Pass coordinator to CoordinatorEntity.""" """Pass coordinator to CoordinatorEntity."""
super().__init__(hub.coordinator) super().__init__(hub.coordinator)
self.robot = robot self.robot = robot
self.entity_type = entity_type if entity_type else "" self.entity_type = entity_type
self.hub = hub self.hub = hub
@property @property
@ -89,22 +89,21 @@ class LitterRobotEntity(CoordinatorEntity):
@property @property
def device_info(self): def device_info(self):
"""Return the device information for a Litter-Robot.""" """Return the device information for a Litter-Robot."""
model = "Litter-Robot 3 Connect"
if not self.robot.serial.startswith("LR3C"):
model = "Other Litter-Robot Connected Device"
return { return {
"identifiers": {(DOMAIN, self.robot.serial)}, "identifiers": {(DOMAIN, self.robot.serial)},
"name": self.robot.name, "name": self.robot.name,
"manufacturer": "Litter-Robot", "manufacturer": "Litter-Robot",
"model": model, "model": self.robot.model,
} }
async def perform_action_and_refresh(self, action: MethodType, *args: Any): async def perform_action_and_refresh(self, action: MethodType, *args: Any):
"""Perform an action and initiates a refresh of the robot data after a few seconds.""" """Perform an action and initiates a refresh of the robot data after a few seconds."""
async def async_call_later_callback(*_) -> None:
await self.hub.coordinator.async_request_refresh()
await action(*args) await action(*args)
async_call_later( async_call_later(self.hass, REFRESH_WAIT_TIME, async_call_later_callback)
self.hass, REFRESH_WAIT_TIME, self.hub.coordinator.async_request_refresh
)
@staticmethod @staticmethod
def parse_time_at_default_timezone(time_str: str) -> Optional[time]: def parse_time_at_default_timezone(time_str: str) -> Optional[time]:

View File

@ -3,6 +3,6 @@
"name": "Litter-Robot", "name": "Litter-Robot",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/litterrobot", "documentation": "https://www.home-assistant.io/integrations/litterrobot",
"requirements": ["pylitterbot==2021.2.5"], "requirements": ["pylitterbot==2021.2.8"],
"codeowners": ["@natekspencer"] "codeowners": ["@natekspencer"]
} }

View File

@ -1,32 +1,44 @@
"""Support for Litter-Robot sensors.""" """Support for Litter-Robot sensors."""
from homeassistant.const import PERCENTAGE from typing import Optional
from pylitterbot.robot import Robot
from homeassistant.const import DEVICE_CLASS_TIMESTAMP, PERCENTAGE
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from .const import DOMAIN from .const import DOMAIN
from .hub import LitterRobotEntity from .hub import LitterRobotEntity, LitterRobotHub
WASTE_DRAWER = "Waste Drawer"
async def async_setup_entry(hass, config_entry, async_add_entities): def icon_for_gauge_level(gauge_level: Optional[int] = None, offset: int = 0) -> str:
"""Set up Litter-Robot sensors using config entry.""" """Return a gauge icon valid identifier."""
hub = hass.data[DOMAIN][config_entry.entry_id] if gauge_level is None or gauge_level <= 0 + offset:
return "mdi:gauge-empty"
entities = [] if gauge_level > 70 + offset:
for robot in hub.account.robots: return "mdi:gauge-full"
entities.append(LitterRobotSensor(robot, WASTE_DRAWER, hub)) if gauge_level > 30 + offset:
return "mdi:gauge"
if entities: return "mdi:gauge-low"
async_add_entities(entities, True)
class LitterRobotSensor(LitterRobotEntity, Entity): class LitterRobotPropertySensor(LitterRobotEntity, Entity):
"""Litter-Robot sensors.""" """Litter-Robot property sensors."""
def __init__(
self, robot: Robot, entity_type: str, hub: LitterRobotHub, sensor_attribute: str
):
"""Pass coordinator to CoordinatorEntity."""
super().__init__(robot, entity_type, hub)
self.sensor_attribute = sensor_attribute
@property @property
def state(self): def state(self):
"""Return the state.""" """Return the state."""
return self.robot.waste_drawer_gauge return getattr(self.robot, self.sensor_attribute)
class LitterRobotWasteSensor(LitterRobotPropertySensor, Entity):
"""Litter-Robot sensors."""
@property @property
def unit_of_measurement(self): def unit_of_measurement(self):
@ -36,19 +48,40 @@ class LitterRobotSensor(LitterRobotEntity, Entity):
@property @property
def icon(self): def icon(self):
"""Return the icon to use in the frontend, if any.""" """Return the icon to use in the frontend, if any."""
if self.robot.waste_drawer_gauge <= 10: return icon_for_gauge_level(self.state, 10)
return "mdi:gauge-empty"
if self.robot.waste_drawer_gauge < 50:
return "mdi:gauge-low" class LitterRobotSleepTimeSensor(LitterRobotPropertySensor, Entity):
if self.robot.waste_drawer_gauge <= 90: """Litter-Robot sleep time sensors."""
return "mdi:gauge"
return "mdi:gauge-full"
@property @property
def device_state_attributes(self): def state(self):
"""Return device specific state attributes.""" """Return the state."""
return { if self.robot.sleep_mode_active:
"cycle_count": self.robot.cycle_count, return super().state.isoformat()
"cycle_capacity": self.robot.cycle_capacity, return None
"cycles_after_drawer_full": self.robot.cycles_after_drawer_full,
} @property
def device_class(self):
"""Return the device class, if any."""
return DEVICE_CLASS_TIMESTAMP
ROBOT_SENSORS = [
(LitterRobotWasteSensor, "Waste Drawer", "waste_drawer_gauge"),
(LitterRobotSleepTimeSensor, "Sleep Mode Start Time", "sleep_mode_start_time"),
(LitterRobotSleepTimeSensor, "Sleep Mode End Time", "sleep_mode_end_time"),
]
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up Litter-Robot sensors using config entry."""
hub = hass.data[DOMAIN][config_entry.entry_id]
entities = []
for robot in hub.account.robots:
for (sensor_class, entity_type, sensor_attribute) in ROBOT_SENSORS:
entities.append(sensor_class(robot, entity_type, hub, sensor_attribute))
if entities:
async_add_entities(entities, True)

View File

@ -1,11 +1,11 @@
"""Support for Litter-Robot switches.""" """Support for Litter-Robot switches."""
from homeassistant.helpers.entity import ToggleEntity from homeassistant.components.switch import SwitchEntity
from .const import DOMAIN from .const import DOMAIN
from .hub import LitterRobotEntity from .hub import LitterRobotEntity
class LitterRobotNightLightModeSwitch(LitterRobotEntity, ToggleEntity): class LitterRobotNightLightModeSwitch(LitterRobotEntity, SwitchEntity):
"""Litter-Robot Night Light Mode Switch.""" """Litter-Robot Night Light Mode Switch."""
@property @property
@ -27,7 +27,7 @@ class LitterRobotNightLightModeSwitch(LitterRobotEntity, ToggleEntity):
await self.perform_action_and_refresh(self.robot.set_night_light, False) await self.perform_action_and_refresh(self.robot.set_night_light, False)
class LitterRobotPanelLockoutSwitch(LitterRobotEntity, ToggleEntity): class LitterRobotPanelLockoutSwitch(LitterRobotEntity, SwitchEntity):
"""Litter-Robot Panel Lockout Switch.""" """Litter-Robot Panel Lockout Switch."""
@property @property

View File

@ -14,7 +14,6 @@ from homeassistant.components.vacuum import (
VacuumEntity, VacuumEntity,
) )
from homeassistant.const import STATE_OFF from homeassistant.const import STATE_OFF
import homeassistant.util.dt as dt_util
from .const import DOMAIN from .const import DOMAIN
from .hub import LitterRobotEntity from .hub import LitterRobotEntity
@ -54,27 +53,22 @@ class LitterRobotCleaner(LitterRobotEntity, VacuumEntity):
def state(self): def state(self):
"""Return the state of the cleaner.""" """Return the state of the cleaner."""
switcher = { switcher = {
Robot.UnitStatus.CCP: STATE_CLEANING, Robot.UnitStatus.CLEAN_CYCLE: STATE_CLEANING,
Robot.UnitStatus.EC: STATE_CLEANING, Robot.UnitStatus.EMPTY_CYCLE: STATE_CLEANING,
Robot.UnitStatus.CCC: STATE_DOCKED, Robot.UnitStatus.CLEAN_CYCLE_COMPLETE: STATE_DOCKED,
Robot.UnitStatus.CST: STATE_DOCKED, Robot.UnitStatus.CAT_SENSOR_TIMING: STATE_DOCKED,
Robot.UnitStatus.DF1: STATE_DOCKED, Robot.UnitStatus.DRAWER_FULL_1: STATE_DOCKED,
Robot.UnitStatus.DF2: STATE_DOCKED, Robot.UnitStatus.DRAWER_FULL_2: STATE_DOCKED,
Robot.UnitStatus.RDY: STATE_DOCKED, Robot.UnitStatus.READY: STATE_DOCKED,
Robot.UnitStatus.OFF: STATE_OFF, Robot.UnitStatus.OFF: STATE_OFF,
} }
return switcher.get(self.robot.unit_status, STATE_ERROR) return switcher.get(self.robot.unit_status, STATE_ERROR)
@property
def error(self):
"""Return the error associated with the current state, if any."""
return self.robot.unit_status.value
@property @property
def status(self): def status(self):
"""Return the status of the cleaner.""" """Return the status of the cleaner."""
return f"{self.robot.unit_status.value}{' (Sleeping)' if self.robot.is_sleeping else ''}" return f"{self.robot.unit_status.label}{' (Sleeping)' if self.robot.is_sleeping else ''}"
async def async_turn_on(self, **kwargs): async def async_turn_on(self, **kwargs):
"""Turn the cleaner on, starting a clean cycle.""" """Turn the cleaner on, starting a clean cycle."""
@ -119,22 +113,11 @@ class LitterRobotCleaner(LitterRobotEntity, VacuumEntity):
@property @property
def device_state_attributes(self): def device_state_attributes(self):
"""Return device specific state attributes.""" """Return device specific state attributes."""
[sleep_mode_start_time, sleep_mode_end_time] = [None, None]
if self.robot.sleep_mode_active:
sleep_mode_start_time = dt_util.as_local(
self.robot.sleep_mode_start_time
).strftime("%H:%M:00")
sleep_mode_end_time = dt_util.as_local(
self.robot.sleep_mode_end_time
).strftime("%H:%M:00")
return { return {
"clean_cycle_wait_time_minutes": self.robot.clean_cycle_wait_time_minutes, "clean_cycle_wait_time_minutes": self.robot.clean_cycle_wait_time_minutes,
"is_sleeping": self.robot.is_sleeping, "is_sleeping": self.robot.is_sleeping,
"sleep_mode_start_time": sleep_mode_start_time, "sleep_mode_active": self.robot.sleep_mode_active,
"sleep_mode_end_time": sleep_mode_end_time,
"power_status": self.robot.power_status, "power_status": self.robot.power_status,
"unit_status_code": self.robot.unit_status.name, "unit_status_code": self.robot.unit_status.value,
"last_seen": self.robot.last_seen, "last_seen": self.robot.last_seen,
} }

View File

@ -1,5 +1,6 @@
"""Connect to a MySensors gateway via pymysensors API.""" """Connect to a MySensors gateway via pymysensors API."""
import asyncio import asyncio
from functools import partial
import logging import logging
from typing import Callable, Dict, List, Optional, Tuple, Type, Union from typing import Callable, Dict, List, Optional, Tuple, Type, Union
@ -7,11 +8,15 @@ from mysensors import BaseAsyncGateway
import voluptuous as vol import voluptuous as vol
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER_DOMAIN
from homeassistant.components.mqtt import valid_publish_topic, valid_subscribe_topic from homeassistant.components.mqtt import valid_publish_topic, valid_subscribe_topic
from homeassistant.components.notify import DOMAIN as NOTIFY_DOMAIN
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_OPTIMISTIC from homeassistant.const import CONF_OPTIMISTIC
from homeassistant.core import callback from homeassistant.core import HomeAssistant, callback
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.discovery import async_load_platform
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.typing import ConfigType, HomeAssistantType from homeassistant.helpers.typing import ConfigType, HomeAssistantType
from .const import ( from .const import (
@ -28,21 +33,24 @@ from .const import (
CONF_TOPIC_OUT_PREFIX, CONF_TOPIC_OUT_PREFIX,
CONF_VERSION, CONF_VERSION,
DOMAIN, DOMAIN,
MYSENSORS_DISCOVERY,
MYSENSORS_GATEWAYS, MYSENSORS_GATEWAYS,
MYSENSORS_ON_UNLOAD, MYSENSORS_ON_UNLOAD,
SUPPORTED_PLATFORMS_WITH_ENTRY_SUPPORT, SUPPORTED_PLATFORMS_WITH_ENTRY_SUPPORT,
DevId, DevId,
GatewayId,
SensorType, SensorType,
) )
from .device import MySensorsDevice, MySensorsEntity, get_mysensors_devices from .device import MySensorsDevice, MySensorsEntity, get_mysensors_devices
from .gateway import finish_setup, get_mysensors_gateway, gw_stop, setup_gateway from .gateway import finish_setup, get_mysensors_gateway, gw_stop, setup_gateway
from .helpers import on_unload
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONF_DEBUG = "debug" CONF_DEBUG = "debug"
CONF_NODE_NAME = "name" CONF_NODE_NAME = "name"
DATA_HASS_CONFIG = "hass_config"
DEFAULT_BAUD_RATE = 115200 DEFAULT_BAUD_RATE = 115200
DEFAULT_TCP_PORT = 5003 DEFAULT_TCP_PORT = 5003
DEFAULT_VERSION = "1.4" DEFAULT_VERSION = "1.4"
@ -134,6 +142,8 @@ CONFIG_SCHEMA = vol.Schema(
async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool:
"""Set up the MySensors component.""" """Set up the MySensors component."""
hass.data[DOMAIN] = {DATA_HASS_CONFIG: config}
if DOMAIN not in config or bool(hass.config_entries.async_entries(DOMAIN)): if DOMAIN not in config or bool(hass.config_entries.async_entries(DOMAIN)):
return True return True
@ -181,14 +191,34 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool
_LOGGER.error("Gateway setup failed for %s", entry.data) _LOGGER.error("Gateway setup failed for %s", entry.data)
return False return False
if DOMAIN not in hass.data:
hass.data[DOMAIN] = {}
if MYSENSORS_GATEWAYS not in hass.data[DOMAIN]: if MYSENSORS_GATEWAYS not in hass.data[DOMAIN]:
hass.data[DOMAIN][MYSENSORS_GATEWAYS] = {} hass.data[DOMAIN][MYSENSORS_GATEWAYS] = {}
hass.data[DOMAIN][MYSENSORS_GATEWAYS][entry.entry_id] = gateway hass.data[DOMAIN][MYSENSORS_GATEWAYS][entry.entry_id] = gateway
async def finish(): # Connect notify discovery as that integration doesn't support entry forwarding.
# Allow loading device tracker platform via discovery
# until refactor to config entry is done.
for platform in (DEVICE_TRACKER_DOMAIN, NOTIFY_DOMAIN):
load_discovery_platform = partial(
async_load_platform,
hass,
platform,
DOMAIN,
hass_config=hass.data[DOMAIN][DATA_HASS_CONFIG],
)
await on_unload(
hass,
entry.entry_id,
async_dispatcher_connect(
hass,
MYSENSORS_DISCOVERY.format(entry.entry_id, platform),
load_discovery_platform,
),
)
async def finish() -> None:
await asyncio.gather( await asyncio.gather(
*[ *[
hass.config_entries.async_forward_entry_setup(entry, platform) hass.config_entries.async_forward_entry_setup(entry, platform)
@ -223,39 +253,24 @@ async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> boo
for fnct in hass.data[DOMAIN][key]: for fnct in hass.data[DOMAIN][key]:
fnct() fnct()
hass.data[DOMAIN].pop(key)
del hass.data[DOMAIN][MYSENSORS_GATEWAYS][entry.entry_id] del hass.data[DOMAIN][MYSENSORS_GATEWAYS][entry.entry_id]
await gw_stop(hass, entry, gateway) await gw_stop(hass, entry, gateway)
return True return True
async def on_unload(
hass: HomeAssistantType, entry: Union[ConfigEntry, GatewayId], fnct: Callable
) -> None:
"""Register a callback to be called when entry is unloaded.
This function is used by platforms to cleanup after themselves
"""
if isinstance(entry, GatewayId):
uniqueid = entry
else:
uniqueid = entry.entry_id
key = MYSENSORS_ON_UNLOAD.format(uniqueid)
if key not in hass.data[DOMAIN]:
hass.data[DOMAIN][key] = []
hass.data[DOMAIN][key].append(fnct)
@callback @callback
def setup_mysensors_platform( def setup_mysensors_platform(
hass, hass: HomeAssistant,
domain: str, # hass platform name domain: str, # hass platform name
discovery_info: Optional[Dict[str, List[DevId]]], discovery_info: Dict[str, List[DevId]],
device_class: Union[Type[MySensorsDevice], Dict[SensorType, Type[MySensorsEntity]]], device_class: Union[Type[MySensorsDevice], Dict[SensorType, Type[MySensorsEntity]]],
device_args: Optional[ device_args: Optional[
Tuple Tuple
] = None, # extra arguments that will be given to the entity constructor ] = None, # extra arguments that will be given to the entity constructor
async_add_entities: Callable = None, async_add_entities: Optional[Callable] = None,
) -> Optional[List[MySensorsDevice]]: ) -> Optional[List[MySensorsDevice]]:
"""Set up a MySensors platform. """Set up a MySensors platform.
@ -264,11 +279,6 @@ def setup_mysensors_platform(
The function is also given a class. The function is also given a class.
A new instance of the class is created for every device id, and the device id is given to the constructor of the class A new instance of the class is created for every device id, and the device id is given to the constructor of the class
""" """
# Only act if called via MySensors by discovery event.
# Otherwise gateway is not set up.
if not discovery_info:
_LOGGER.debug("Skipping setup due to no discovery info")
return None
if device_args is None: if device_args is None:
device_args = () device_args = ()
new_devices: List[MySensorsDevice] = [] new_devices: List[MySensorsDevice] = []

View File

@ -12,6 +12,9 @@ async def async_setup_scanner(
hass: HomeAssistantType, config, async_see, discovery_info=None hass: HomeAssistantType, config, async_see, discovery_info=None
): ):
"""Set up the MySensors device scanner.""" """Set up the MySensors device scanner."""
if not discovery_info:
return False
new_devices = mysensors.setup_mysensors_platform( new_devices = mysensors.setup_mysensors_platform(
hass, hass,
DOMAIN, DOMAIN,

View File

@ -31,7 +31,12 @@ from .const import (
GatewayId, GatewayId,
) )
from .handler import HANDLERS from .handler import HANDLERS
from .helpers import discover_mysensors_platform, validate_child, validate_node from .helpers import (
discover_mysensors_platform,
on_unload,
validate_child,
validate_node,
)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -260,8 +265,8 @@ async def _discover_persistent_devices(
async def gw_stop(hass, entry: ConfigEntry, gateway: BaseAsyncGateway): async def gw_stop(hass, entry: ConfigEntry, gateway: BaseAsyncGateway):
"""Stop the gateway.""" """Stop the gateway."""
connect_task = hass.data[DOMAIN].get( connect_task = hass.data[DOMAIN].pop(
MYSENSORS_GATEWAY_START_TASK.format(entry.entry_id) MYSENSORS_GATEWAY_START_TASK.format(entry.entry_id), None
) )
if connect_task is not None and not connect_task.done(): if connect_task is not None and not connect_task.done():
connect_task.cancel() connect_task.cancel()
@ -288,7 +293,12 @@ async def _gw_start(
async def stop_this_gw(_: Event): async def stop_this_gw(_: Event):
await gw_stop(hass, entry, gateway) await gw_stop(hass, entry, gateway)
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_this_gw) await on_unload(
hass,
entry.entry_id,
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_this_gw),
)
if entry.data[CONF_DEVICE] == MQTT_COMPONENT: if entry.data[CONF_DEVICE] == MQTT_COMPONENT:
# Gatways connected via mqtt doesn't send gateway ready message. # Gatways connected via mqtt doesn't send gateway ready message.
return return

View File

@ -2,16 +2,18 @@
from collections import defaultdict from collections import defaultdict
from enum import IntEnum from enum import IntEnum
import logging import logging
from typing import DefaultDict, Dict, List, Optional, Set from typing import Callable, DefaultDict, Dict, List, Optional, Set, Union
from mysensors import BaseAsyncGateway, Message from mysensors import BaseAsyncGateway, Message
from mysensors.sensor import ChildSensor from mysensors.sensor import ChildSensor
import voluptuous as vol import voluptuous as vol
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_NAME from homeassistant.const import CONF_NAME
from homeassistant.core import callback from homeassistant.core import HomeAssistant, callback
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.util.decorator import Registry from homeassistant.util.decorator import Registry
from .const import ( from .const import (
@ -20,6 +22,7 @@ from .const import (
DOMAIN, DOMAIN,
FLAT_PLATFORM_TYPES, FLAT_PLATFORM_TYPES,
MYSENSORS_DISCOVERY, MYSENSORS_DISCOVERY,
MYSENSORS_ON_UNLOAD,
TYPE_TO_PLATFORMS, TYPE_TO_PLATFORMS,
DevId, DevId,
GatewayId, GatewayId,
@ -31,9 +34,26 @@ _LOGGER = logging.getLogger(__name__)
SCHEMAS = Registry() SCHEMAS = Registry()
async def on_unload(
hass: HomeAssistantType, entry: Union[ConfigEntry, GatewayId], fnct: Callable
) -> None:
"""Register a callback to be called when entry is unloaded.
This function is used by platforms to cleanup after themselves.
"""
if isinstance(entry, GatewayId):
uniqueid = entry
else:
uniqueid = entry.entry_id
key = MYSENSORS_ON_UNLOAD.format(uniqueid)
if key not in hass.data[DOMAIN]:
hass.data[DOMAIN][key] = []
hass.data[DOMAIN][key].append(fnct)
@callback @callback
def discover_mysensors_platform( def discover_mysensors_platform(
hass, gateway_id: GatewayId, platform: str, new_devices: List[DevId] hass: HomeAssistant, gateway_id: GatewayId, platform: str, new_devices: List[DevId]
) -> None: ) -> None:
"""Discover a MySensors platform.""" """Discover a MySensors platform."""
_LOGGER.debug("Discovering platform %s with devIds: %s", platform, new_devices) _LOGGER.debug("Discovering platform %s with devIds: %s", platform, new_devices)

View File

@ -2,15 +2,8 @@
"domain": "mysensors", "domain": "mysensors",
"name": "MySensors", "name": "MySensors",
"documentation": "https://www.home-assistant.io/integrations/mysensors", "documentation": "https://www.home-assistant.io/integrations/mysensors",
"requirements": [ "requirements": ["pymysensors==0.21.0"],
"pymysensors==0.20.1" "after_dependencies": ["mqtt"],
], "codeowners": ["@MartinHjelmare", "@functionpointer"],
"after_dependencies": [
"mqtt"
],
"codeowners": [
"@MartinHjelmare",
"@functionpointer"
],
"config_flow": true "config_flow": true
} }

View File

@ -5,6 +5,9 @@ from homeassistant.components.notify import ATTR_TARGET, DOMAIN, BaseNotificatio
async def async_get_service(hass, config, discovery_info=None): async def async_get_service(hass, config, discovery_info=None):
"""Get the MySensors notification service.""" """Get the MySensors notification service."""
if not discovery_info:
return None
new_devices = mysensors.setup_mysensors_platform( new_devices = mysensors.setup_mysensors_platform(
hass, DOMAIN, discovery_info, MySensorsNotificationDevice hass, DOMAIN, discovery_info, MySensorsNotificationDevice
) )

View File

@ -139,7 +139,9 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator):
def _convert_forecast(self, entry): def _convert_forecast(self, entry):
forecast = { forecast = {
ATTR_FORECAST_TIME: dt.utc_from_timestamp(entry.reference_time("unix")), ATTR_FORECAST_TIME: dt.utc_from_timestamp(
entry.reference_time("unix")
).isoformat(),
ATTR_FORECAST_PRECIPITATION: self._calc_precipitation( ATTR_FORECAST_PRECIPITATION: self._calc_precipitation(
entry.rain, entry.snow entry.rain, entry.snow
), ),

View File

@ -7,8 +7,7 @@
"python-openzwave-mqtt[mqtt-client]==1.4.0" "python-openzwave-mqtt[mqtt-client]==1.4.0"
], ],
"after_dependencies": [ "after_dependencies": [
"mqtt", "mqtt"
"zwave"
], ],
"codeowners": [ "codeowners": [
"@cgarwood", "@cgarwood",

View File

@ -177,9 +177,9 @@ def get_device_wrapper(hass: HomeAssistant, device_id: str):
return None return None
for config_entry in hass.data[DOMAIN][DATA_CONFIG_ENTRY]: for config_entry in hass.data[DOMAIN][DATA_CONFIG_ENTRY]:
wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry][COAP] wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry].get(COAP)
if wrapper.device_id == device_id: if wrapper and wrapper.device_id == device_id:
return wrapper return wrapper
return None return None

View File

@ -646,9 +646,12 @@ class SonosEntity(MediaPlayerEntity):
update_position = new_status != self._status update_position = new_status != self._status
self._status = new_status self._status = new_status
track_uri = variables["current_track_uri"] if variables else None if variables:
track_uri = variables["current_track_uri"]
music_source = self.soco.music_source_from_uri(track_uri) music_source = self.soco.music_source_from_uri(track_uri)
else:
# This causes a network round-trip so we avoid it when possible
music_source = self.soco.music_source
if music_source == MUSIC_SRC_TV: if music_source == MUSIC_SRC_TV:
self.update_media_linein(SOURCE_TV) self.update_media_linein(SOURCE_TV)

View File

@ -523,7 +523,6 @@ class TemplateFan(TemplateEntity, FanEntity):
speed = str(speed) speed = str(speed)
if speed in self._speed_list: if speed in self._speed_list:
self._state = STATE_OFF if speed == SPEED_OFF else STATE_ON
self._speed = speed self._speed = speed
self._percentage = self.speed_to_percentage(speed) self._percentage = self.speed_to_percentage(speed)
self._preset_mode = speed if speed in self.preset_modes else None self._preset_mode = speed if speed in self.preset_modes else None
@ -552,7 +551,6 @@ class TemplateFan(TemplateEntity, FanEntity):
return return
if 0 <= percentage <= 100: if 0 <= percentage <= 100:
self._state = STATE_OFF if percentage == 0 else STATE_ON
self._percentage = percentage self._percentage = percentage
if self._speed_list: if self._speed_list:
self._speed = self.percentage_to_speed(percentage) self._speed = self.percentage_to_speed(percentage)
@ -569,7 +567,6 @@ class TemplateFan(TemplateEntity, FanEntity):
preset_mode = str(preset_mode) preset_mode = str(preset_mode)
if preset_mode in self.preset_modes: if preset_mode in self.preset_modes:
self._state = STATE_ON
self._speed = preset_mode self._speed = preset_mode
self._percentage = None self._percentage = None
self._preset_mode = preset_mode self._preset_mode = preset_mode

View File

@ -162,6 +162,15 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity):
add_to_watched_value_ids=True, add_to_watched_value_ids=True,
) )
self._set_modes_and_presets() self._set_modes_and_presets()
self._supported_features = SUPPORT_PRESET_MODE
# If any setpoint value exists, we can assume temperature
# can be set
if any(self._setpoint_values.values()):
self._supported_features |= SUPPORT_TARGET_TEMPERATURE
if HVAC_MODE_HEAT_COOL in self.hvac_modes:
self._supported_features |= SUPPORT_TARGET_TEMPERATURE_RANGE
if self._fan_mode:
self._supported_features |= SUPPORT_FAN_MODE
def _setpoint_value(self, setpoint_type: ThermostatSetpointType) -> ZwaveValue: def _setpoint_value(self, setpoint_type: ThermostatSetpointType) -> ZwaveValue:
"""Optionally return a ZwaveValue for a setpoint.""" """Optionally return a ZwaveValue for a setpoint."""
@ -259,7 +268,7 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity):
return None return None
try: try:
temp = self._setpoint_value(self._current_mode_setpoint_enums[0]) temp = self._setpoint_value(self._current_mode_setpoint_enums[0])
except ValueError: except (IndexError, ValueError):
return None return None
return temp.value if temp else None return temp.value if temp else None
@ -271,7 +280,7 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity):
return None return None
try: try:
temp = self._setpoint_value(self._current_mode_setpoint_enums[1]) temp = self._setpoint_value(self._current_mode_setpoint_enums[1])
except ValueError: except (IndexError, ValueError):
return None return None
return temp.value if temp else None return temp.value if temp else None
@ -335,14 +344,7 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity):
@property @property
def supported_features(self) -> int: def supported_features(self) -> int:
"""Return the list of supported features.""" """Return the list of supported features."""
support = SUPPORT_PRESET_MODE return self._supported_features
if len(self._current_mode_setpoint_enums) == 1:
support |= SUPPORT_TARGET_TEMPERATURE
if len(self._current_mode_setpoint_enums) > 1:
support |= SUPPORT_TARGET_TEMPERATURE_RANGE
if self._fan_mode:
support |= SUPPORT_FAN_MODE
return support
async def async_set_fan_mode(self, fan_mode: str) -> None: async def async_set_fan_mode(self, fan_mode: str) -> None:
"""Set new target fan mode.""" """Set new target fan mode."""

View File

@ -105,7 +105,11 @@ class ZWaveBaseEntity(Entity):
"""Generate entity name.""" """Generate entity name."""
if additional_info is None: if additional_info is None:
additional_info = [] additional_info = []
name: str = self.info.node.name or self.info.node.device_config.description name: str = (
self.info.node.name
or self.info.node.device_config.description
or f"Node {self.info.node.node_id}"
)
if include_value_name: if include_value_name:
value_name = ( value_name = (
alternate_value_name alternate_value_name

View File

@ -1,7 +1,7 @@
"""Constants used by Home Assistant components.""" """Constants used by Home Assistant components."""
MAJOR_VERSION = 2021 MAJOR_VERSION = 2021
MINOR_VERSION = 3 MINOR_VERSION = 3
PATCH_VERSION = "2" PATCH_VERSION = "3"
__short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__ = f"{__short_version__}.{PATCH_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER = (3, 8, 0) REQUIRED_PYTHON_VER = (3, 8, 0)

View File

@ -15,7 +15,7 @@ defusedxml==0.6.0
distro==1.5.0 distro==1.5.0
emoji==1.2.0 emoji==1.2.0
hass-nabucasa==0.41.0 hass-nabucasa==0.41.0
home-assistant-frontend==20210302.5 home-assistant-frontend==20210302.6
httpx==0.16.1 httpx==0.16.1
jinja2>=2.11.3 jinja2>=2.11.3
netdisco==2.8.2 netdisco==2.8.2

View File

@ -17,7 +17,7 @@ Adafruit-SHT31==1.0.2
# Adafruit_BBIO==1.1.1 # Adafruit_BBIO==1.1.1
# homeassistant.components.homekit # homeassistant.components.homekit
HAP-python==3.3.2 HAP-python==3.4.0
# homeassistant.components.mastodon # homeassistant.components.mastodon
Mastodon.py==1.5.1 Mastodon.py==1.5.1
@ -763,7 +763,7 @@ hole==0.5.1
holidays==0.10.5.2 holidays==0.10.5.2
# homeassistant.components.frontend # homeassistant.components.frontend
home-assistant-frontend==20210302.5 home-assistant-frontend==20210302.6
# homeassistant.components.zwave # homeassistant.components.zwave
homeassistant-pyozw==0.1.10 homeassistant-pyozw==0.1.10
@ -1504,7 +1504,7 @@ pylibrespot-java==0.1.0
pylitejet==0.3.0 pylitejet==0.3.0
# homeassistant.components.litterrobot # homeassistant.components.litterrobot
pylitterbot==2021.2.5 pylitterbot==2021.2.8
# homeassistant.components.loopenergy # homeassistant.components.loopenergy
pyloopenergy==0.2.1 pyloopenergy==0.2.1
@ -1555,7 +1555,7 @@ pymusiccast==0.1.6
pymyq==3.0.4 pymyq==3.0.4
# homeassistant.components.mysensors # homeassistant.components.mysensors
pymysensors==0.20.1 pymysensors==0.21.0
# homeassistant.components.nanoleaf # homeassistant.components.nanoleaf
pynanoleaf==0.0.5 pynanoleaf==0.0.5

View File

@ -7,7 +7,7 @@
AEMET-OpenData==0.1.8 AEMET-OpenData==0.1.8
# homeassistant.components.homekit # homeassistant.components.homekit
HAP-python==3.3.2 HAP-python==3.4.0
# homeassistant.components.flick_electric # homeassistant.components.flick_electric
PyFlick==0.0.2 PyFlick==0.0.2
@ -412,7 +412,7 @@ hole==0.5.1
holidays==0.10.5.2 holidays==0.10.5.2
# homeassistant.components.frontend # homeassistant.components.frontend
home-assistant-frontend==20210302.5 home-assistant-frontend==20210302.6
# homeassistant.components.zwave # homeassistant.components.zwave
homeassistant-pyozw==0.1.10 homeassistant-pyozw==0.1.10
@ -794,7 +794,7 @@ pylibrespot-java==0.1.0
pylitejet==0.3.0 pylitejet==0.3.0
# homeassistant.components.litterrobot # homeassistant.components.litterrobot
pylitterbot==2021.2.5 pylitterbot==2021.2.8
# homeassistant.components.lutron_caseta # homeassistant.components.lutron_caseta
pylutron-caseta==0.9.0 pylutron-caseta==0.9.0
@ -827,7 +827,7 @@ pymonoprice==0.3
pymyq==3.0.4 pymyq==3.0.4
# homeassistant.components.mysensors # homeassistant.components.mysensors
pymysensors==0.20.1 pymysensors==0.21.0
# homeassistant.components.nuki # homeassistant.components.nuki
pynuki==1.3.8 pynuki==1.3.8

View File

@ -147,6 +147,8 @@ IGNORE_VIOLATIONS = {
# Demo # Demo
("demo", "manual"), ("demo", "manual"),
("demo", "openalpr_local"), ("demo", "openalpr_local"),
# Migration wizard from zwave to ozw.
"ozw",
# This should become a helper method that integrations can submit data to # This should become a helper method that integrations can submit data to
("websocket_api", "lovelace"), ("websocket_api", "lovelace"),
("websocket_api", "shopping_list"), ("websocket_api", "shopping_list"),

View File

@ -37,7 +37,9 @@ async def test_aemet_forecast_create_sensors(hass):
assert state.state == "-4" assert state.state == "-4"
state = hass.states.get("sensor.aemet_daily_forecast_time") state = hass.states.get("sensor.aemet_daily_forecast_time")
assert state.state == "2021-01-10 00:00:00+00:00" assert (
state.state == dt_util.parse_datetime("2021-01-10 00:00:00+00:00").isoformat()
)
state = hass.states.get("sensor.aemet_daily_forecast_wind_bearing") state = hass.states.get("sensor.aemet_daily_forecast_wind_bearing")
assert state.state == "45.0" assert state.state == "45.0"

View File

@ -51,8 +51,9 @@ async def test_aemet_weather(hass):
assert forecast.get(ATTR_FORECAST_PRECIPITATION_PROBABILITY) == 30 assert forecast.get(ATTR_FORECAST_PRECIPITATION_PROBABILITY) == 30
assert forecast.get(ATTR_FORECAST_TEMP) == 4 assert forecast.get(ATTR_FORECAST_TEMP) == 4
assert forecast.get(ATTR_FORECAST_TEMP_LOW) == -4 assert forecast.get(ATTR_FORECAST_TEMP_LOW) == -4
assert forecast.get(ATTR_FORECAST_TIME) == dt_util.parse_datetime( assert (
"2021-01-10 00:00:00+00:00" forecast.get(ATTR_FORECAST_TIME)
== dt_util.parse_datetime("2021-01-10 00:00:00+00:00").isoformat()
) )
assert forecast.get(ATTR_FORECAST_WIND_BEARING) == 45.0 assert forecast.get(ATTR_FORECAST_WIND_BEARING) == 45.0
assert forecast.get(ATTR_FORECAST_WIND_SPEED) == 20 assert forecast.get(ATTR_FORECAST_WIND_SPEED) == 20

View File

@ -1,6 +1,6 @@
"""Tests for the AsusWrt config flow.""" """Tests for the AsusWrt config flow."""
from socket import gaierror from socket import gaierror
from unittest.mock import AsyncMock, patch from unittest.mock import AsyncMock, Mock, patch
import pytest import pytest
@ -46,7 +46,7 @@ def mock_controller_connect():
with patch("homeassistant.components.asuswrt.router.AsusWrt") as service_mock: with patch("homeassistant.components.asuswrt.router.AsusWrt") as service_mock:
service_mock.return_value.connection.async_connect = AsyncMock() service_mock.return_value.connection.async_connect = AsyncMock()
service_mock.return_value.is_connected = True service_mock.return_value.is_connected = True
service_mock.return_value.connection.disconnect = AsyncMock() service_mock.return_value.connection.disconnect = Mock()
yield service_mock yield service_mock

View File

@ -1,6 +1,6 @@
"""Tests for the AsusWrt sensor.""" """Tests for the AsusWrt sensor."""
from datetime import timedelta from datetime import timedelta
from unittest.mock import AsyncMock, patch from unittest.mock import AsyncMock, Mock, patch
from aioasuswrt.asuswrt import Device from aioasuswrt.asuswrt import Device
import pytest import pytest
@ -49,7 +49,7 @@ def mock_controller_connect():
with patch("homeassistant.components.asuswrt.router.AsusWrt") as service_mock: with patch("homeassistant.components.asuswrt.router.AsusWrt") as service_mock:
service_mock.return_value.connection.async_connect = AsyncMock() service_mock.return_value.connection.async_connect = AsyncMock()
service_mock.return_value.is_connected = True service_mock.return_value.is_connected = True
service_mock.return_value.connection.disconnect = AsyncMock() service_mock.return_value.connection.disconnect = Mock()
service_mock.return_value.async_get_connected_devices = AsyncMock( service_mock.return_value.async_get_connected_devices = AsyncMock(
return_value=MOCK_DEVICES return_value=MOCK_DEVICES
) )

View File

@ -0,0 +1,169 @@
"""Tests for the Bond entities."""
import asyncio
from datetime import timedelta
from unittest.mock import patch
from bond_api import BPUPSubscriptions, DeviceType
from homeassistant import core
from homeassistant.components import fan
from homeassistant.components.fan import DOMAIN as FAN_DOMAIN
from homeassistant.const import STATE_ON, STATE_UNAVAILABLE
from homeassistant.util import utcnow
from .common import patch_bond_device_state, setup_platform
from tests.common import async_fire_time_changed
def ceiling_fan(name: str):
"""Create a ceiling fan with given name."""
return {
"name": name,
"type": DeviceType.CEILING_FAN,
"actions": ["SetSpeed", "SetDirection"],
}
async def test_bpup_goes_offline_and_recovers_same_entity(hass: core.HomeAssistant):
"""Test that push updates fail and we fallback to polling and then bpup recovers.
The BPUP recovery is triggered by an update for the entity and
we do not fallback to polling because state is in sync.
"""
bpup_subs = BPUPSubscriptions()
with patch(
"homeassistant.components.bond.BPUPSubscriptions",
return_value=bpup_subs,
):
await setup_platform(
hass, FAN_DOMAIN, ceiling_fan("name-1"), bond_device_id="test-device-id"
)
bpup_subs.notify(
{
"s": 200,
"t": "bond/test-device-id/update",
"b": {"power": 1, "speed": 3, "direction": 0},
}
)
await hass.async_block_till_done()
assert hass.states.get("fan.name_1").attributes[fan.ATTR_PERCENTAGE] == 100
bpup_subs.notify(
{
"s": 200,
"t": "bond/test-device-id/update",
"b": {"power": 1, "speed": 1, "direction": 0},
}
)
await hass.async_block_till_done()
assert hass.states.get("fan.name_1").attributes[fan.ATTR_PERCENTAGE] == 33
bpup_subs.last_message_time = 0
with patch_bond_device_state(side_effect=asyncio.TimeoutError):
async_fire_time_changed(hass, utcnow() + timedelta(seconds=230))
await hass.async_block_till_done()
assert hass.states.get("fan.name_1").state == STATE_UNAVAILABLE
# Ensure we do not poll to get the state
# since bpup has recovered and we know we
# are back in sync
with patch_bond_device_state(side_effect=Exception):
bpup_subs.notify(
{
"s": 200,
"t": "bond/test-device-id/update",
"b": {"power": 1, "speed": 2, "direction": 0},
}
)
await hass.async_block_till_done()
state = hass.states.get("fan.name_1")
assert state.state == STATE_ON
assert state.attributes[fan.ATTR_PERCENTAGE] == 66
async def test_bpup_goes_offline_and_recovers_different_entity(
hass: core.HomeAssistant,
):
"""Test that push updates fail and we fallback to polling and then bpup recovers.
The BPUP recovery is triggered by an update for a different entity which
forces a poll since we need to re-get the state.
"""
bpup_subs = BPUPSubscriptions()
with patch(
"homeassistant.components.bond.BPUPSubscriptions",
return_value=bpup_subs,
):
await setup_platform(
hass, FAN_DOMAIN, ceiling_fan("name-1"), bond_device_id="test-device-id"
)
bpup_subs.notify(
{
"s": 200,
"t": "bond/test-device-id/update",
"b": {"power": 1, "speed": 3, "direction": 0},
}
)
await hass.async_block_till_done()
assert hass.states.get("fan.name_1").attributes[fan.ATTR_PERCENTAGE] == 100
bpup_subs.notify(
{
"s": 200,
"t": "bond/test-device-id/update",
"b": {"power": 1, "speed": 1, "direction": 0},
}
)
await hass.async_block_till_done()
assert hass.states.get("fan.name_1").attributes[fan.ATTR_PERCENTAGE] == 33
bpup_subs.last_message_time = 0
with patch_bond_device_state(side_effect=asyncio.TimeoutError):
async_fire_time_changed(hass, utcnow() + timedelta(seconds=230))
await hass.async_block_till_done()
assert hass.states.get("fan.name_1").state == STATE_UNAVAILABLE
bpup_subs.notify(
{
"s": 200,
"t": "bond/not-this-device-id/update",
"b": {"power": 1, "speed": 2, "direction": 0},
}
)
await hass.async_block_till_done()
assert hass.states.get("fan.name_1").state == STATE_UNAVAILABLE
with patch_bond_device_state(return_value={"power": 1, "speed": 1}):
async_fire_time_changed(hass, utcnow() + timedelta(seconds=430))
await hass.async_block_till_done()
state = hass.states.get("fan.name_1")
assert state.state == STATE_ON
assert state.attributes[fan.ATTR_PERCENTAGE] == 33
async def test_polling_fails_and_recovers(hass: core.HomeAssistant):
"""Test that polling fails and we recover."""
await setup_platform(
hass, FAN_DOMAIN, ceiling_fan("name-1"), bond_device_id="test-device-id"
)
with patch_bond_device_state(side_effect=asyncio.TimeoutError):
async_fire_time_changed(hass, utcnow() + timedelta(seconds=230))
await hass.async_block_till_done()
assert hass.states.get("fan.name_1").state == STATE_UNAVAILABLE
with patch_bond_device_state(return_value={"power": 1, "speed": 1}):
async_fire_time_changed(hass, utcnow() + timedelta(seconds=230))
await hass.async_block_till_done()
state = hass.states.get("fan.name_1")
assert state.state == STATE_ON
assert state.attributes[fan.ATTR_PERCENTAGE] == 33

View File

@ -493,7 +493,7 @@ async def test_homekit_start(hass, hk_driver, device_reg):
) as mock_setup_msg, patch( ) as mock_setup_msg, patch(
"pyhap.accessory_driver.AccessoryDriver.add_accessory" "pyhap.accessory_driver.AccessoryDriver.add_accessory"
) as hk_driver_add_acc, patch( ) as hk_driver_add_acc, patch(
"pyhap.accessory_driver.AccessoryDriver.start_service" "pyhap.accessory_driver.AccessoryDriver.async_start"
) as hk_driver_start: ) as hk_driver_start:
await homekit.async_start() await homekit.async_start()
@ -528,7 +528,7 @@ async def test_homekit_start(hass, hk_driver, device_reg):
) as mock_setup_msg, patch( ) as mock_setup_msg, patch(
"pyhap.accessory_driver.AccessoryDriver.add_accessory" "pyhap.accessory_driver.AccessoryDriver.add_accessory"
) as hk_driver_add_acc, patch( ) as hk_driver_add_acc, patch(
"pyhap.accessory_driver.AccessoryDriver.start_service" "pyhap.accessory_driver.AccessoryDriver.async_start"
) as hk_driver_start: ) as hk_driver_start:
await homekit.async_start() await homekit.async_start()
@ -567,7 +567,7 @@ async def test_homekit_start_with_a_broken_accessory(hass, hk_driver, mock_zeroc
) as mock_setup_msg, patch( ) as mock_setup_msg, patch(
"pyhap.accessory_driver.AccessoryDriver.add_accessory", "pyhap.accessory_driver.AccessoryDriver.add_accessory",
) as hk_driver_add_acc, patch( ) as hk_driver_add_acc, patch(
"pyhap.accessory_driver.AccessoryDriver.start_service" "pyhap.accessory_driver.AccessoryDriver.async_start"
) as hk_driver_start: ) as hk_driver_start:
await homekit.async_start() await homekit.async_start()
@ -630,7 +630,7 @@ async def test_homekit_reset_accessories(hass, mock_zeroconf):
), patch("pyhap.accessory.Bridge.add_accessory") as mock_add_accessory, patch( ), patch("pyhap.accessory.Bridge.add_accessory") as mock_add_accessory, patch(
"pyhap.accessory_driver.AccessoryDriver.config_changed" "pyhap.accessory_driver.AccessoryDriver.config_changed"
) as hk_driver_config_changed, patch( ) as hk_driver_config_changed, patch(
"pyhap.accessory_driver.AccessoryDriver.start_service" "pyhap.accessory_driver.AccessoryDriver.async_start"
): ):
await async_init_entry(hass, entry) await async_init_entry(hass, entry)
@ -674,7 +674,7 @@ async def test_homekit_too_many_accessories(hass, hk_driver, caplog, mock_zeroco
hass.states.async_set("light.demo2", "on") hass.states.async_set("light.demo2", "on")
hass.states.async_set("light.demo3", "on") hass.states.async_set("light.demo3", "on")
with patch("pyhap.accessory_driver.AccessoryDriver.start_service"), patch( with patch("pyhap.accessory_driver.AccessoryDriver.async_start"), patch(
"pyhap.accessory_driver.AccessoryDriver.add_accessory" "pyhap.accessory_driver.AccessoryDriver.add_accessory"
), patch(f"{PATH_HOMEKIT}.show_setup_message"), patch( ), patch(f"{PATH_HOMEKIT}.show_setup_message"), patch(
f"{PATH_HOMEKIT}.HomeBridge", _mock_bridge f"{PATH_HOMEKIT}.HomeBridge", _mock_bridge
@ -738,7 +738,7 @@ async def test_homekit_finds_linked_batteries(
with patch.object(homekit.bridge, "add_accessory"), patch( with patch.object(homekit.bridge, "add_accessory"), patch(
f"{PATH_HOMEKIT}.show_setup_message" f"{PATH_HOMEKIT}.show_setup_message"
), patch(f"{PATH_HOMEKIT}.get_accessory") as mock_get_acc, patch( ), patch(f"{PATH_HOMEKIT}.get_accessory") as mock_get_acc, patch(
"pyhap.accessory_driver.AccessoryDriver.start_service" "pyhap.accessory_driver.AccessoryDriver.async_start"
): ):
await homekit.async_start() await homekit.async_start()
await hass.async_block_till_done() await hass.async_block_till_done()
@ -810,7 +810,7 @@ async def test_homekit_async_get_integration_fails(
with patch.object(homekit.bridge, "add_accessory"), patch( with patch.object(homekit.bridge, "add_accessory"), patch(
f"{PATH_HOMEKIT}.show_setup_message" f"{PATH_HOMEKIT}.show_setup_message"
), patch(f"{PATH_HOMEKIT}.get_accessory") as mock_get_acc, patch( ), patch(f"{PATH_HOMEKIT}.get_accessory") as mock_get_acc, patch(
"pyhap.accessory_driver.AccessoryDriver.start_service" "pyhap.accessory_driver.AccessoryDriver.async_start"
): ):
await homekit.async_start() await homekit.async_start()
await hass.async_block_till_done() await hass.async_block_till_done()
@ -895,7 +895,7 @@ async def test_homekit_uses_system_zeroconf(hass, hk_driver, mock_zeroconf):
assert await async_setup_component(hass, "zeroconf", {"zeroconf": {}}) assert await async_setup_component(hass, "zeroconf", {"zeroconf": {}})
system_zc = await zeroconf.async_get_instance(hass) system_zc = await zeroconf.async_get_instance(hass)
with patch("pyhap.accessory_driver.AccessoryDriver.start_service"), patch( with patch("pyhap.accessory_driver.AccessoryDriver.async_start"), patch(
f"{PATH_HOMEKIT}.HomeKit.async_stop" f"{PATH_HOMEKIT}.HomeKit.async_stop"
): ):
entry.add_to_hass(hass) entry.add_to_hass(hass)
@ -963,7 +963,7 @@ async def test_homekit_ignored_missing_devices(
with patch.object(homekit.bridge, "add_accessory"), patch( with patch.object(homekit.bridge, "add_accessory"), patch(
f"{PATH_HOMEKIT}.show_setup_message" f"{PATH_HOMEKIT}.show_setup_message"
), patch(f"{PATH_HOMEKIT}.get_accessory") as mock_get_acc, patch( ), patch(f"{PATH_HOMEKIT}.get_accessory") as mock_get_acc, patch(
"pyhap.accessory_driver.AccessoryDriver.start_service" "pyhap.accessory_driver.AccessoryDriver.async_start"
): ):
await homekit.async_start() await homekit.async_start()
await hass.async_block_till_done() await hass.async_block_till_done()
@ -1025,7 +1025,7 @@ async def test_homekit_finds_linked_motion_sensors(
with patch.object(homekit.bridge, "add_accessory"), patch( with patch.object(homekit.bridge, "add_accessory"), patch(
f"{PATH_HOMEKIT}.show_setup_message" f"{PATH_HOMEKIT}.show_setup_message"
), patch(f"{PATH_HOMEKIT}.get_accessory") as mock_get_acc, patch( ), patch(f"{PATH_HOMEKIT}.get_accessory") as mock_get_acc, patch(
"pyhap.accessory_driver.AccessoryDriver.start_service" "pyhap.accessory_driver.AccessoryDriver.async_start"
): ):
await homekit.async_start() await homekit.async_start()
await hass.async_block_till_done() await hass.async_block_till_done()
@ -1090,7 +1090,7 @@ async def test_homekit_finds_linked_humidity_sensors(
with patch.object(homekit.bridge, "add_accessory"), patch( with patch.object(homekit.bridge, "add_accessory"), patch(
f"{PATH_HOMEKIT}.show_setup_message" f"{PATH_HOMEKIT}.show_setup_message"
), patch(f"{PATH_HOMEKIT}.get_accessory") as mock_get_acc, patch( ), patch(f"{PATH_HOMEKIT}.get_accessory") as mock_get_acc, patch(
"pyhap.accessory_driver.AccessoryDriver.start_service" "pyhap.accessory_driver.AccessoryDriver.async_start"
): ):
await homekit.async_start() await homekit.async_start()
await hass.async_block_till_done() await hass.async_block_till_done()
@ -1153,7 +1153,7 @@ async def test_reload(hass, mock_zeroconf):
), patch( ), patch(
f"{PATH_HOMEKIT}.get_accessory" f"{PATH_HOMEKIT}.get_accessory"
), patch( ), patch(
"pyhap.accessory_driver.AccessoryDriver.start_service" "pyhap.accessory_driver.AccessoryDriver.async_start"
): ):
mock_homekit2.return_value = homekit = Mock() mock_homekit2.return_value = homekit = Mock()
type(homekit).async_start = AsyncMock() type(homekit).async_start = AsyncMock()
@ -1205,7 +1205,7 @@ async def test_homekit_start_in_accessory_mode(hass, hk_driver, device_reg):
with patch(f"{PATH_HOMEKIT}.HomeKit.add_bridge_accessory") as mock_add_acc, patch( with patch(f"{PATH_HOMEKIT}.HomeKit.add_bridge_accessory") as mock_add_acc, patch(
"pyhap.accessory_driver.AccessoryDriver.add_accessory" "pyhap.accessory_driver.AccessoryDriver.add_accessory"
), patch(f"{PATH_HOMEKIT}.show_setup_message") as mock_setup_msg, patch( ), patch(f"{PATH_HOMEKIT}.show_setup_message") as mock_setup_msg, patch(
"pyhap.accessory_driver.AccessoryDriver.start_service" "pyhap.accessory_driver.AccessoryDriver.async_start"
) as hk_driver_start: ) as hk_driver_start:
await homekit.async_start() await homekit.async_start()

View File

@ -11,6 +11,13 @@ import homeassistant.util.dt as dt_util
from tests.components.light.conftest import mock_light_profiles # noqa from tests.components.light.conftest import mock_light_profiles # noqa
@pytest.fixture(autouse=True)
def mock_zeroconf():
"""Mock zeroconf."""
with mock.patch("homeassistant.components.zeroconf.HaZeroconf") as mock_zc:
yield mock_zc.return_value
@pytest.fixture @pytest.fixture
def utcnow(request): def utcnow(request):
"""Freeze time at a known point.""" """Freeze time at a known point."""

View File

@ -50,6 +50,38 @@ def create_fanv2_service(accessory):
swing_mode.value = 0 swing_mode.value = 0
def create_fanv2_service_with_min_step(accessory):
"""Define fan v2 characteristics as per HAP spec."""
service = accessory.add_service(ServicesTypes.FAN_V2)
cur_state = service.add_char(CharacteristicsTypes.ACTIVE)
cur_state.value = 0
direction = service.add_char(CharacteristicsTypes.ROTATION_DIRECTION)
direction.value = 0
speed = service.add_char(CharacteristicsTypes.ROTATION_SPEED)
speed.value = 0
speed.minStep = 25
swing_mode = service.add_char(CharacteristicsTypes.SWING_MODE)
swing_mode.value = 0
def create_fanv2_service_without_rotation_speed(accessory):
"""Define fan v2 characteristics as per HAP spec."""
service = accessory.add_service(ServicesTypes.FAN_V2)
cur_state = service.add_char(CharacteristicsTypes.ACTIVE)
cur_state.value = 0
direction = service.add_char(CharacteristicsTypes.ROTATION_DIRECTION)
direction.value = 0
swing_mode = service.add_char(CharacteristicsTypes.SWING_MODE)
swing_mode.value = 0
async def test_fan_read_state(hass, utcnow): async def test_fan_read_state(hass, utcnow):
"""Test that we can read the state of a HomeKit fan accessory.""" """Test that we can read the state of a HomeKit fan accessory."""
helper = await setup_test_component(hass, create_fan_service) helper = await setup_test_component(hass, create_fan_service)
@ -95,6 +127,29 @@ async def test_turn_on(hass, utcnow):
assert helper.characteristics[V1_ROTATION_SPEED].value == 33.0 assert helper.characteristics[V1_ROTATION_SPEED].value == 33.0
async def test_turn_on_off_without_rotation_speed(hass, utcnow):
"""Test that we can turn a fan on."""
helper = await setup_test_component(
hass, create_fanv2_service_without_rotation_speed
)
await hass.services.async_call(
"fan",
"turn_on",
{"entity_id": "fan.testdevice"},
blocking=True,
)
assert helper.characteristics[V2_ACTIVE].value == 1
await hass.services.async_call(
"fan",
"turn_off",
{"entity_id": "fan.testdevice"},
blocking=True,
)
assert helper.characteristics[V2_ACTIVE].value == 0
async def test_turn_off(hass, utcnow): async def test_turn_off(hass, utcnow):
"""Test that we can turn a fan off.""" """Test that we can turn a fan off."""
helper = await setup_test_component(hass, create_fan_service) helper = await setup_test_component(hass, create_fan_service)
@ -181,6 +236,7 @@ async def test_speed_read(hass, utcnow):
state = await helper.poll_and_get_state() state = await helper.poll_and_get_state()
assert state.attributes["speed"] == "high" assert state.attributes["speed"] == "high"
assert state.attributes["percentage"] == 100 assert state.attributes["percentage"] == 100
assert state.attributes["percentage_step"] == 1.0
helper.characteristics[V1_ROTATION_SPEED].value = 50 helper.characteristics[V1_ROTATION_SPEED].value = 50
state = await helper.poll_and_get_state() state = await helper.poll_and_get_state()
@ -277,6 +333,24 @@ async def test_v2_turn_on(hass, utcnow):
assert helper.characteristics[V2_ACTIVE].value == 1 assert helper.characteristics[V2_ACTIVE].value == 1
assert helper.characteristics[V2_ROTATION_SPEED].value == 33.0 assert helper.characteristics[V2_ROTATION_SPEED].value == 33.0
await hass.services.async_call(
"fan",
"turn_off",
{"entity_id": "fan.testdevice"},
blocking=True,
)
assert helper.characteristics[V2_ACTIVE].value == 0
assert helper.characteristics[V2_ROTATION_SPEED].value == 33.0
await hass.services.async_call(
"fan",
"turn_on",
{"entity_id": "fan.testdevice"},
blocking=True,
)
assert helper.characteristics[V2_ACTIVE].value == 1
assert helper.characteristics[V2_ROTATION_SPEED].value == 33.0
async def test_v2_turn_off(hass, utcnow): async def test_v2_turn_off(hass, utcnow):
"""Test that we can turn a fan off.""" """Test that we can turn a fan off."""
@ -355,6 +429,29 @@ async def test_v2_set_percentage(hass, utcnow):
assert helper.characteristics[V2_ACTIVE].value == 0 assert helper.characteristics[V2_ACTIVE].value == 0
async def test_v2_set_percentage_with_min_step(hass, utcnow):
"""Test that we set fan speed by percentage."""
helper = await setup_test_component(hass, create_fanv2_service_with_min_step)
helper.characteristics[V2_ACTIVE].value = 1
await hass.services.async_call(
"fan",
"set_percentage",
{"entity_id": "fan.testdevice", "percentage": 66},
blocking=True,
)
assert helper.characteristics[V2_ROTATION_SPEED].value == 75
await hass.services.async_call(
"fan",
"set_percentage",
{"entity_id": "fan.testdevice", "percentage": 0},
blocking=True,
)
assert helper.characteristics[V2_ACTIVE].value == 0
async def test_v2_speed_read(hass, utcnow): async def test_v2_speed_read(hass, utcnow):
"""Test that we can read a fans oscillation.""" """Test that we can read a fans oscillation."""
helper = await setup_test_component(hass, create_fanv2_service) helper = await setup_test_component(hass, create_fanv2_service)

View File

@ -1,45 +1,59 @@
"""Configure pytest for Litter-Robot tests.""" """Configure pytest for Litter-Robot tests."""
from typing import Optional
from unittest.mock import AsyncMock, MagicMock, patch from unittest.mock import AsyncMock, MagicMock, patch
import pylitterbot
from pylitterbot import Robot from pylitterbot import Robot
import pytest import pytest
from homeassistant.components import litterrobot from homeassistant.components import litterrobot
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .common import CONFIG, ROBOT_DATA from .common import CONFIG, ROBOT_DATA
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
def create_mock_robot(hass): def create_mock_robot(unit_status_code: Optional[str] = None):
"""Create a mock Litter-Robot device.""" """Create a mock Litter-Robot device."""
robot = Robot(data=ROBOT_DATA) if not (
robot.start_cleaning = AsyncMock() unit_status_code
robot.set_power_status = AsyncMock() and Robot.UnitStatus(unit_status_code) != Robot.UnitStatus.UNKNOWN
robot.reset_waste_drawer = AsyncMock() ):
robot.set_sleep_mode = AsyncMock() unit_status_code = ROBOT_DATA["unitStatus"]
robot.set_night_light = AsyncMock()
robot.set_panel_lockout = AsyncMock() with patch.dict(ROBOT_DATA, {"unitStatus": unit_status_code}):
return robot robot = Robot(data=ROBOT_DATA)
robot.start_cleaning = AsyncMock()
robot.set_power_status = AsyncMock()
robot.reset_waste_drawer = AsyncMock()
robot.set_sleep_mode = AsyncMock()
robot.set_night_light = AsyncMock()
robot.set_panel_lockout = AsyncMock()
return robot
@pytest.fixture() def create_mock_account(unit_status_code: Optional[str] = None):
def mock_hub(hass): """Create a mock Litter-Robot account."""
"""Mock a Litter-Robot hub.""" account = MagicMock(spec=pylitterbot.Account)
hub = MagicMock( account.connect = AsyncMock()
hass=hass, account.refresh_robots = AsyncMock()
account=MagicMock(), account.robots = [create_mock_robot(unit_status_code)]
logged_in=True, return account
coordinator=MagicMock(spec=DataUpdateCoordinator),
spec=litterrobot.LitterRobotHub,
)
hub.coordinator.last_update_success = True
hub.account.robots = [create_mock_robot(hass)]
return hub
async def setup_hub(hass, mock_hub, platform_domain): @pytest.fixture
def mock_account():
"""Mock a Litter-Robot account."""
return create_mock_account()
@pytest.fixture
def mock_account_with_error():
"""Mock a Litter-Robot account with error."""
return create_mock_account("BR")
async def setup_integration(hass, mock_account, platform_domain=None):
"""Load a Litter-Robot platform with the provided hub.""" """Load a Litter-Robot platform with the provided hub."""
entry = MockConfigEntry( entry = MockConfigEntry(
domain=litterrobot.DOMAIN, domain=litterrobot.DOMAIN,
@ -47,9 +61,11 @@ async def setup_hub(hass, mock_hub, platform_domain):
) )
entry.add_to_hass(hass) entry.add_to_hass(hass)
with patch( with patch("pylitterbot.Account", return_value=mock_account), patch(
"homeassistant.components.litterrobot.LitterRobotHub", "homeassistant.components.litterrobot.PLATFORMS",
return_value=mock_hub, [platform_domain] if platform_domain else [],
): ):
await hass.config_entries.async_forward_entry_setup(entry, platform_domain) await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
return entry

View File

@ -4,11 +4,14 @@ from unittest.mock import patch
from pylitterbot.exceptions import LitterRobotException, LitterRobotLoginException from pylitterbot.exceptions import LitterRobotException, LitterRobotLoginException
from homeassistant import config_entries, setup from homeassistant import config_entries, setup
from homeassistant.components import litterrobot
from .common import CONF_USERNAME, CONFIG, DOMAIN from .common import CONF_USERNAME, CONFIG, DOMAIN
from tests.common import MockConfigEntry
async def test_form(hass):
async def test_form(hass, mock_account):
"""Test we get the form.""" """Test we get the form."""
await setup.async_setup_component(hass, "persistent_notification", {}) await setup.async_setup_component(hass, "persistent_notification", {})
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
@ -17,10 +20,7 @@ async def test_form(hass):
assert result["type"] == "form" assert result["type"] == "form"
assert result["errors"] == {} assert result["errors"] == {}
with patch( with patch("pylitterbot.Account", return_value=mock_account), patch(
"homeassistant.components.litterrobot.config_flow.LitterRobotHub.login",
return_value=True,
), patch(
"homeassistant.components.litterrobot.async_setup", return_value=True "homeassistant.components.litterrobot.async_setup", return_value=True
) as mock_setup, patch( ) as mock_setup, patch(
"homeassistant.components.litterrobot.async_setup_entry", "homeassistant.components.litterrobot.async_setup_entry",
@ -38,6 +38,23 @@ async def test_form(hass):
assert len(mock_setup_entry.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1
async def test_already_configured(hass):
"""Test we handle already configured."""
MockConfigEntry(
domain=litterrobot.DOMAIN,
data=CONFIG[litterrobot.DOMAIN],
).add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_USER},
data=CONFIG[litterrobot.DOMAIN],
)
assert result["type"] == "abort"
assert result["reason"] == "already_configured"
async def test_form_invalid_auth(hass): async def test_form_invalid_auth(hass):
"""Test we handle invalid auth.""" """Test we handle invalid auth."""
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
@ -45,7 +62,7 @@ async def test_form_invalid_auth(hass):
) )
with patch( with patch(
"homeassistant.components.litterrobot.config_flow.LitterRobotHub.login", "pylitterbot.Account.connect",
side_effect=LitterRobotLoginException, side_effect=LitterRobotLoginException,
): ):
result2 = await hass.config_entries.flow.async_configure( result2 = await hass.config_entries.flow.async_configure(
@ -63,7 +80,7 @@ async def test_form_cannot_connect(hass):
) )
with patch( with patch(
"homeassistant.components.litterrobot.config_flow.LitterRobotHub.login", "pylitterbot.Account.connect",
side_effect=LitterRobotException, side_effect=LitterRobotException,
): ):
result2 = await hass.config_entries.flow.async_configure( result2 = await hass.config_entries.flow.async_configure(
@ -81,7 +98,7 @@ async def test_form_unknown_error(hass):
) )
with patch( with patch(
"homeassistant.components.litterrobot.config_flow.LitterRobotHub.login", "pylitterbot.Account.connect",
side_effect=Exception, side_effect=Exception,
): ):
result2 = await hass.config_entries.flow.async_configure( result2 = await hass.config_entries.flow.async_configure(

View File

@ -1,20 +1,48 @@
"""Test Litter-Robot setup process.""" """Test Litter-Robot setup process."""
from unittest.mock import patch
from pylitterbot.exceptions import LitterRobotException, LitterRobotLoginException
import pytest
from homeassistant.components import litterrobot from homeassistant.components import litterrobot
from homeassistant.setup import async_setup_component from homeassistant.config_entries import (
ENTRY_STATE_SETUP_ERROR,
ENTRY_STATE_SETUP_RETRY,
)
from .common import CONFIG from .common import CONFIG
from .conftest import setup_integration
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
async def test_unload_entry(hass): async def test_unload_entry(hass, mock_account):
"""Test being able to unload an entry.""" """Test being able to unload an entry."""
entry = await setup_integration(hass, mock_account)
assert await hass.config_entries.async_unload(entry.entry_id)
await hass.async_block_till_done()
assert hass.data[litterrobot.DOMAIN] == {}
@pytest.mark.parametrize(
"side_effect,expected_state",
(
(LitterRobotLoginException, ENTRY_STATE_SETUP_ERROR),
(LitterRobotException, ENTRY_STATE_SETUP_RETRY),
),
)
async def test_entry_not_setup(hass, side_effect, expected_state):
"""Test being able to handle config entry not setup."""
entry = MockConfigEntry( entry = MockConfigEntry(
domain=litterrobot.DOMAIN, domain=litterrobot.DOMAIN,
data=CONFIG[litterrobot.DOMAIN], data=CONFIG[litterrobot.DOMAIN],
) )
entry.add_to_hass(hass) entry.add_to_hass(hass)
assert await async_setup_component(hass, litterrobot.DOMAIN, {}) is True with patch(
assert await litterrobot.async_unload_entry(hass, entry) "pylitterbot.Account.connect",
assert hass.data[litterrobot.DOMAIN] == {} side_effect=side_effect,
):
await hass.config_entries.async_setup(entry.entry_id)
assert entry.state == expected_state

View File

@ -1,20 +1,57 @@
"""Test the Litter-Robot sensor entity.""" """Test the Litter-Robot sensor entity."""
from unittest.mock import Mock
from homeassistant.components.litterrobot.sensor import LitterRobotSleepTimeSensor
from homeassistant.components.sensor import DOMAIN as PLATFORM_DOMAIN from homeassistant.components.sensor import DOMAIN as PLATFORM_DOMAIN
from homeassistant.const import PERCENTAGE from homeassistant.const import DEVICE_CLASS_TIMESTAMP, PERCENTAGE
from .conftest import setup_hub from .conftest import create_mock_robot, setup_integration
ENTITY_ID = "sensor.test_waste_drawer" WASTE_DRAWER_ENTITY_ID = "sensor.test_waste_drawer"
async def test_sensor(hass, mock_hub): async def test_waste_drawer_sensor(hass, mock_account):
"""Tests the sensor entity was set up.""" """Tests the waste drawer sensor entity was set up."""
await setup_hub(hass, mock_hub, PLATFORM_DOMAIN) await setup_integration(hass, mock_account, PLATFORM_DOMAIN)
sensor = hass.states.get(ENTITY_ID) sensor = hass.states.get(WASTE_DRAWER_ENTITY_ID)
assert sensor assert sensor
assert sensor.state == "50" assert sensor.state == "50"
assert sensor.attributes["cycle_count"] == 15
assert sensor.attributes["cycle_capacity"] == 30
assert sensor.attributes["cycles_after_drawer_full"] == 0
assert sensor.attributes["unit_of_measurement"] == PERCENTAGE assert sensor.attributes["unit_of_measurement"] == PERCENTAGE
async def test_sleep_time_sensor_with_none_state(hass):
"""Tests the sleep mode start time sensor where sleep mode is inactive."""
robot = create_mock_robot()
robot.sleep_mode_active = False
sensor = LitterRobotSleepTimeSensor(
robot, "Sleep Mode Start Time", Mock(), "sleep_mode_start_time"
)
assert sensor
assert sensor.state is None
assert sensor.device_class == DEVICE_CLASS_TIMESTAMP
async def test_gauge_icon():
"""Test icon generator for gauge sensor."""
from homeassistant.components.litterrobot.sensor import icon_for_gauge_level
GAUGE_EMPTY = "mdi:gauge-empty"
GAUGE_LOW = "mdi:gauge-low"
GAUGE = "mdi:gauge"
GAUGE_FULL = "mdi:gauge-full"
assert icon_for_gauge_level(None) == GAUGE_EMPTY
assert icon_for_gauge_level(0) == GAUGE_EMPTY
assert icon_for_gauge_level(5) == GAUGE_LOW
assert icon_for_gauge_level(40) == GAUGE
assert icon_for_gauge_level(80) == GAUGE_FULL
assert icon_for_gauge_level(100) == GAUGE_FULL
assert icon_for_gauge_level(None, 10) == GAUGE_EMPTY
assert icon_for_gauge_level(0, 10) == GAUGE_EMPTY
assert icon_for_gauge_level(5, 10) == GAUGE_EMPTY
assert icon_for_gauge_level(40, 10) == GAUGE_LOW
assert icon_for_gauge_level(80, 10) == GAUGE
assert icon_for_gauge_level(100, 10) == GAUGE_FULL

View File

@ -12,7 +12,7 @@ from homeassistant.components.switch import (
from homeassistant.const import ATTR_ENTITY_ID, STATE_ON from homeassistant.const import ATTR_ENTITY_ID, STATE_ON
from homeassistant.util.dt import utcnow from homeassistant.util.dt import utcnow
from .conftest import setup_hub from .conftest import setup_integration
from tests.common import async_fire_time_changed from tests.common import async_fire_time_changed
@ -20,9 +20,9 @@ NIGHT_LIGHT_MODE_ENTITY_ID = "switch.test_night_light_mode"
PANEL_LOCKOUT_ENTITY_ID = "switch.test_panel_lockout" PANEL_LOCKOUT_ENTITY_ID = "switch.test_panel_lockout"
async def test_switch(hass, mock_hub): async def test_switch(hass, mock_account):
"""Tests the switch entity was set up.""" """Tests the switch entity was set up."""
await setup_hub(hass, mock_hub, PLATFORM_DOMAIN) await setup_integration(hass, mock_account, PLATFORM_DOMAIN)
switch = hass.states.get(NIGHT_LIGHT_MODE_ENTITY_ID) switch = hass.states.get(NIGHT_LIGHT_MODE_ENTITY_ID)
assert switch assert switch
@ -36,9 +36,9 @@ async def test_switch(hass, mock_hub):
(PANEL_LOCKOUT_ENTITY_ID, "set_panel_lockout"), (PANEL_LOCKOUT_ENTITY_ID, "set_panel_lockout"),
], ],
) )
async def test_on_off_commands(hass, mock_hub, entity_id, robot_command): async def test_on_off_commands(hass, mock_account, entity_id, robot_command):
"""Test sending commands to the switch.""" """Test sending commands to the switch."""
await setup_hub(hass, mock_hub, PLATFORM_DOMAIN) await setup_integration(hass, mock_account, PLATFORM_DOMAIN)
switch = hass.states.get(entity_id) switch = hass.states.get(entity_id)
assert switch assert switch
@ -48,12 +48,14 @@ async def test_on_off_commands(hass, mock_hub, entity_id, robot_command):
count = 0 count = 0
for service in [SERVICE_TURN_ON, SERVICE_TURN_OFF]: for service in [SERVICE_TURN_ON, SERVICE_TURN_OFF]:
count += 1 count += 1
await hass.services.async_call( await hass.services.async_call(
PLATFORM_DOMAIN, PLATFORM_DOMAIN,
service, service,
data, data,
blocking=True, blocking=True,
) )
future = utcnow() + timedelta(seconds=REFRESH_WAIT_TIME) future = utcnow() + timedelta(seconds=REFRESH_WAIT_TIME)
async_fire_time_changed(hass, future) async_fire_time_changed(hass, future)
assert getattr(mock_hub.account.robots[0], robot_command).call_count == count assert getattr(mock_account.robots[0], robot_command).call_count == count

View File

@ -12,20 +12,21 @@ from homeassistant.components.vacuum import (
SERVICE_TURN_OFF, SERVICE_TURN_OFF,
SERVICE_TURN_ON, SERVICE_TURN_ON,
STATE_DOCKED, STATE_DOCKED,
STATE_ERROR,
) )
from homeassistant.const import ATTR_COMMAND, ATTR_ENTITY_ID from homeassistant.const import ATTR_COMMAND, ATTR_ENTITY_ID
from homeassistant.util.dt import utcnow from homeassistant.util.dt import utcnow
from .conftest import setup_hub from .conftest import setup_integration
from tests.common import async_fire_time_changed from tests.common import async_fire_time_changed
ENTITY_ID = "vacuum.test_litter_box" ENTITY_ID = "vacuum.test_litter_box"
async def test_vacuum(hass, mock_hub): async def test_vacuum(hass, mock_account):
"""Tests the vacuum entity was set up.""" """Tests the vacuum entity was set up."""
await setup_hub(hass, mock_hub, PLATFORM_DOMAIN) await setup_integration(hass, mock_account, PLATFORM_DOMAIN)
vacuum = hass.states.get(ENTITY_ID) vacuum = hass.states.get(ENTITY_ID)
assert vacuum assert vacuum
@ -33,6 +34,15 @@ async def test_vacuum(hass, mock_hub):
assert vacuum.attributes["is_sleeping"] is False assert vacuum.attributes["is_sleeping"] is False
async def test_vacuum_with_error(hass, mock_account_with_error):
"""Tests a vacuum entity with an error."""
await setup_integration(hass, mock_account_with_error, PLATFORM_DOMAIN)
vacuum = hass.states.get(ENTITY_ID)
assert vacuum
assert vacuum.state == STATE_ERROR
@pytest.mark.parametrize( @pytest.mark.parametrize(
"service,command,extra", "service,command,extra",
[ [
@ -52,14 +62,22 @@ async def test_vacuum(hass, mock_hub):
ATTR_PARAMS: {"enabled": True, "sleep_time": "22:30"}, ATTR_PARAMS: {"enabled": True, "sleep_time": "22:30"},
}, },
), ),
(
SERVICE_SEND_COMMAND,
"set_sleep_mode",
{
ATTR_COMMAND: "set_sleep_mode",
ATTR_PARAMS: {"enabled": True, "sleep_time": None},
},
),
], ],
) )
async def test_commands(hass, mock_hub, service, command, extra): async def test_commands(hass, mock_account, service, command, extra):
"""Test sending commands to the vacuum.""" """Test sending commands to the vacuum."""
await setup_hub(hass, mock_hub, PLATFORM_DOMAIN) await setup_integration(hass, mock_account, PLATFORM_DOMAIN)
vacuum = hass.states.get(ENTITY_ID) vacuum = hass.states.get(ENTITY_ID)
assert vacuum is not None assert vacuum
assert vacuum.state == STATE_DOCKED assert vacuum.state == STATE_DOCKED
data = {ATTR_ENTITY_ID: ENTITY_ID} data = {ATTR_ENTITY_ID: ENTITY_ID}
@ -74,4 +92,4 @@ async def test_commands(hass, mock_hub, service, command, extra):
) )
future = utcnow() + timedelta(seconds=REFRESH_WAIT_TIME) future = utcnow() + timedelta(seconds=REFRESH_WAIT_TIME)
async_fire_time_changed(hass, future) async_fire_time_changed(hass, future)
getattr(mock_hub.account.robots[0], command).assert_called_once() getattr(mock_account.robots[0], command).assert_called_once()

View File

@ -246,6 +246,10 @@ async def test_templates_with_entities(hass, calls):
await hass.async_block_till_done() await hass.async_block_till_done()
_verify(hass, STATE_ON, None, 0, True, DIRECTION_FORWARD, None) _verify(hass, STATE_ON, None, 0, True, DIRECTION_FORWARD, None)
hass.states.async_set(_STATE_INPUT_BOOLEAN, False)
await hass.async_block_till_done()
_verify(hass, STATE_OFF, None, 0, True, DIRECTION_FORWARD, None)
async def test_templates_with_entities_and_invalid_percentage(hass, calls): async def test_templates_with_entities_and_invalid_percentage(hass, calls):
"""Test templates with values from other entities.""" """Test templates with values from other entities."""
@ -274,7 +278,7 @@ async def test_templates_with_entities_and_invalid_percentage(hass, calls):
await hass.async_start() await hass.async_start()
await hass.async_block_till_done() await hass.async_block_till_done()
_verify(hass, STATE_OFF, SPEED_OFF, 0, None, None, None) _verify(hass, STATE_ON, SPEED_OFF, 0, None, None, None)
hass.states.async_set("sensor.percentage", "33") hass.states.async_set("sensor.percentage", "33")
await hass.async_block_till_done() await hass.async_block_till_done()
@ -299,7 +303,7 @@ async def test_templates_with_entities_and_invalid_percentage(hass, calls):
hass.states.async_set("sensor.percentage", "0") hass.states.async_set("sensor.percentage", "0")
await hass.async_block_till_done() await hass.async_block_till_done()
_verify(hass, STATE_OFF, SPEED_OFF, 0, None, None, None) _verify(hass, STATE_ON, SPEED_OFF, 0, None, None, None)
async def test_templates_with_entities_and_preset_modes(hass, calls): async def test_templates_with_entities_and_preset_modes(hass, calls):

View File

@ -288,6 +288,18 @@ def aeotec_radiator_thermostat_state_fixture():
return json.loads(load_fixture("zwave_js/aeotec_radiator_thermostat_state.json")) return json.loads(load_fixture("zwave_js/aeotec_radiator_thermostat_state.json"))
@pytest.fixture(name="inovelli_lzw36_state", scope="session")
def inovelli_lzw36_state_fixture():
"""Load the Inovelli LZW36 node state fixture data."""
return json.loads(load_fixture("zwave_js/inovelli_lzw36_state.json"))
@pytest.fixture(name="null_name_check_state", scope="session")
def null_name_check_state_fixture():
"""Load the null name check node state fixture data."""
return json.loads(load_fixture("zwave_js/null_name_check_state.json"))
@pytest.fixture(name="client") @pytest.fixture(name="client")
def mock_client_fixture(controller_state, version_state): def mock_client_fixture(controller_state, version_state):
"""Mock a client.""" """Mock a client."""
@ -484,6 +496,14 @@ def in_wall_smart_fan_control_fixture(client, in_wall_smart_fan_control_state):
return node return node
@pytest.fixture(name="null_name_check")
def null_name_check_fixture(client, null_name_check_state):
"""Mock a node with no name."""
node = Node(client, copy.deepcopy(null_name_check_state))
client.driver.controller.nodes[node.node_id] = node
return node
@pytest.fixture(name="multiple_devices") @pytest.fixture(name="multiple_devices")
def multiple_devices_fixture( def multiple_devices_fixture(
client, climate_radio_thermostat_ct100_plus_state, lock_schlage_be469_state client, climate_radio_thermostat_ct100_plus_state, lock_schlage_be469_state

View File

@ -24,9 +24,17 @@ from homeassistant.components.climate.const import (
SERVICE_SET_HVAC_MODE, SERVICE_SET_HVAC_MODE,
SERVICE_SET_PRESET_MODE, SERVICE_SET_PRESET_MODE,
SERVICE_SET_TEMPERATURE, SERVICE_SET_TEMPERATURE,
SUPPORT_FAN_MODE,
SUPPORT_PRESET_MODE,
SUPPORT_TARGET_TEMPERATURE,
SUPPORT_TARGET_TEMPERATURE_RANGE,
) )
from homeassistant.components.zwave_js.climate import ATTR_FAN_STATE from homeassistant.components.zwave_js.climate import ATTR_FAN_STATE
from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE from homeassistant.const import (
ATTR_ENTITY_ID,
ATTR_SUPPORTED_FEATURES,
ATTR_TEMPERATURE,
)
from .common import ( from .common import (
CLIMATE_DANFOSS_LC13_ENTITY, CLIMATE_DANFOSS_LC13_ENTITY,
@ -58,6 +66,13 @@ async def test_thermostat_v2(
assert state.attributes[ATTR_PRESET_MODE] == PRESET_NONE assert state.attributes[ATTR_PRESET_MODE] == PRESET_NONE
assert state.attributes[ATTR_FAN_MODE] == "Auto low" assert state.attributes[ATTR_FAN_MODE] == "Auto low"
assert state.attributes[ATTR_FAN_STATE] == "Idle / off" assert state.attributes[ATTR_FAN_STATE] == "Idle / off"
assert (
state.attributes[ATTR_SUPPORTED_FEATURES]
== SUPPORT_PRESET_MODE
| SUPPORT_TARGET_TEMPERATURE
| SUPPORT_TARGET_TEMPERATURE_RANGE
| SUPPORT_FAN_MODE
)
# Test setting preset mode # Test setting preset mode
await hass.services.async_call( await hass.services.async_call(
@ -408,6 +423,10 @@ async def test_setpoint_thermostat(hass, client, climate_danfoss_lc_13, integrat
assert state.attributes[ATTR_TEMPERATURE] == 14 assert state.attributes[ATTR_TEMPERATURE] == 14
assert state.attributes[ATTR_HVAC_MODES] == [HVAC_MODE_HEAT] assert state.attributes[ATTR_HVAC_MODES] == [HVAC_MODE_HEAT]
assert state.attributes[ATTR_PRESET_MODE] == PRESET_NONE assert state.attributes[ATTR_PRESET_MODE] == PRESET_NONE
assert (
state.attributes[ATTR_SUPPORTED_FEATURES]
== SUPPORT_PRESET_MODE | SUPPORT_TARGET_TEMPERATURE
)
client.async_send_command_no_wait.reset_mock() client.async_send_command_no_wait.reset_mock()
@ -491,6 +510,10 @@ async def test_thermostat_heatit(hass, client, climate_heatit_z_trm3, integratio
assert state.attributes[ATTR_TEMPERATURE] == 22.5 assert state.attributes[ATTR_TEMPERATURE] == 22.5
assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE
assert state.attributes[ATTR_PRESET_MODE] == PRESET_NONE assert state.attributes[ATTR_PRESET_MODE] == PRESET_NONE
assert (
state.attributes[ATTR_SUPPORTED_FEATURES]
== SUPPORT_PRESET_MODE | SUPPORT_TARGET_TEMPERATURE
)
async def test_thermostat_srt321_hrt4_zw(hass, client, srt321_hrt4_zw, integration): async def test_thermostat_srt321_hrt4_zw(hass, client, srt321_hrt4_zw, integration):
@ -507,3 +530,4 @@ async def test_thermostat_srt321_hrt4_zw(hass, client, srt321_hrt4_zw, integrati
HVAC_MODE_HEAT, HVAC_MODE_HEAT,
] ]
assert state.attributes[ATTR_CURRENT_TEMPERATURE] is None assert state.attributes[ATTR_CURRENT_TEMPERATURE] is None
assert state.attributes[ATTR_SUPPORTED_FEATURES] == SUPPORT_PRESET_MODE

View File

@ -459,6 +459,12 @@ async def test_existing_node_ready(
) )
async def test_null_name(hass, client, null_name_check, integration):
"""Test that node without a name gets a generic node name."""
node = null_name_check
assert hass.states.get(f"switch.node_{node.node_id}")
async def test_existing_node_not_ready(hass, client, multisensor_6, device_registry): async def test_existing_node_not_ready(hass, client, multisensor_6, device_registry):
"""Test we handle a non ready node that exists during integration setup.""" """Test we handle a non ready node that exists during integration setup."""
node = multisensor_6 node = multisensor_6

View File

@ -0,0 +1,414 @@
{
"nodeId": 10,
"index": 0,
"installerIcon": 3328,
"userIcon": 3328,
"status": 4,
"ready": true,
"isListening": true,
"isFrequentListening": false,
"isRouting": true,
"maxBaudRate": 40000,
"isSecure": false,
"version": 4,
"isBeaming": true,
"manufacturerId": 277,
"productId": 1,
"productType": 272,
"firmwareVersion": "2.17",
"zwavePlusVersion": 1,
"nodeType": 0,
"roleType": 1,
"neighbors": [],
"endpointCountIsDynamic": false,
"endpointsHaveIdenticalCapabilities": false,
"individualEndpointCount": 4,
"aggregatedEndpointCount": 0,
"interviewAttempts": 1,
"interviewStage": 7,
"endpoints": [
{
"nodeId": 10,
"index": 0,
"installerIcon": 3328,
"userIcon": 3328
},
{
"nodeId": 10,
"index": 1
},
{
"nodeId": 10,
"index": 2
},
{
"nodeId": 10,
"index": 3
},
{
"nodeId": 10,
"index": 4
}
],
"values": [
{
"endpoint": 0,
"commandClass": 49,
"commandClassName": "Multilevel Sensor",
"property": "Air temperature",
"propertyName": "Air temperature",
"ccVersion": 7,
"metadata": {
"type": "number",
"readable": true,
"writeable": false,
"unit": "\u00b0C",
"label": "Air temperature",
"ccSpecific": {
"sensorType": 1,
"scale": 0
}
},
"value": 2.9
},
{
"endpoint": 0,
"commandClass": 49,
"commandClassName": "Multilevel Sensor",
"property": "Humidity",
"propertyName": "Humidity",
"ccVersion": 7,
"metadata": {
"type": "number",
"readable": true,
"writeable": false,
"unit": "%",
"label": "Humidity",
"ccSpecific": {
"sensorType": 5,
"scale": 0
}
},
"value": 8
},
{
"endpoint": 0,
"commandClass": 114,
"commandClassName": "Manufacturer Specific",
"property": "manufacturerId",
"propertyName": "manufacturerId",
"ccVersion": 2,
"metadata": {
"type": "number",
"readable": true,
"writeable": false,
"min": 0,
"max": 65535,
"label": "Manufacturer ID"
},
"value": 277
},
{
"endpoint": 0,
"commandClass": 114,
"commandClassName": "Manufacturer Specific",
"property": "productType",
"propertyName": "productType",
"ccVersion": 2,
"metadata": {
"type": "number",
"readable": true,
"writeable": false,
"min": 0,
"max": 65535,
"label": "Product type"
},
"value": 272
},
{
"endpoint": 0,
"commandClass": 114,
"commandClassName": "Manufacturer Specific",
"property": "productId",
"propertyName": "productId",
"ccVersion": 2,
"metadata": {
"type": "number",
"readable": true,
"writeable": false,
"min": 0,
"max": 65535,
"label": "Product ID"
},
"value": 1
},
{
"endpoint": 0,
"commandClass": 134,
"commandClassName": "Version",
"property": "libraryType",
"propertyName": "libraryType",
"ccVersion": 2,
"metadata": {
"type": "any",
"readable": true,
"writeable": false,
"label": "Library type"
},
"value": 3
},
{
"endpoint": 0,
"commandClass": 134,
"commandClassName": "Version",
"property": "protocolVersion",
"propertyName": "protocolVersion",
"ccVersion": 2,
"metadata": {
"type": "any",
"readable": true,
"writeable": false,
"label": "Z-Wave protocol version"
},
"value": "4.38"
},
{
"endpoint": 0,
"commandClass": 134,
"commandClassName": "Version",
"property": "firmwareVersions",
"propertyName": "firmwareVersions",
"ccVersion": 2,
"metadata": {
"type": "any",
"readable": true,
"writeable": false,
"label": "Z-Wave chip firmware versions"
},
"value": ["2.17"]
},
{
"endpoint": 0,
"commandClass": 134,
"commandClassName": "Version",
"property": "hardwareVersion",
"propertyName": "hardwareVersion",
"ccVersion": 2,
"metadata": {
"type": "any",
"readable": true,
"writeable": false,
"label": "Z-Wave chip hardware version"
}
},
{
"endpoint": 1,
"commandClass": 37,
"commandClassName": "Binary Switch",
"property": "currentValue",
"propertyName": "currentValue",
"ccVersion": 1,
"metadata": {
"type": "boolean",
"readable": true,
"writeable": false,
"label": "Current value"
},
"value": false
},
{
"endpoint": 1,
"commandClass": 37,
"commandClassName": "Binary Switch",
"property": "targetValue",
"propertyName": "targetValue",
"ccVersion": 1,
"metadata": {
"type": "boolean",
"readable": true,
"writeable": true,
"label": "Target value"
},
"value": false
},
{
"endpoint": 2,
"commandClass": 37,
"commandClassName": "Binary Switch",
"property": "currentValue",
"propertyName": "currentValue",
"ccVersion": 1,
"metadata": {
"type": "boolean",
"readable": true,
"writeable": false,
"label": "Current value"
},
"value": false
},
{
"endpoint": 2,
"commandClass": 37,
"commandClassName": "Binary Switch",
"property": "targetValue",
"propertyName": "targetValue",
"ccVersion": 1,
"metadata": {
"type": "boolean",
"readable": true,
"writeable": true,
"label": "Target value"
},
"value": false
},
{
"endpoint": 3,
"commandClass": 37,
"commandClassName": "Binary Switch",
"property": "currentValue",
"propertyName": "currentValue",
"ccVersion": 1,
"metadata": {
"type": "boolean",
"readable": true,
"writeable": false,
"label": "Current value"
},
"value": false
},
{
"endpoint": 3,
"commandClass": 37,
"commandClassName": "Binary Switch",
"property": "targetValue",
"propertyName": "targetValue",
"ccVersion": 1,
"metadata": {
"type": "boolean",
"readable": true,
"writeable": true,
"label": "Target value"
}
},
{
"endpoint": 4,
"commandClass": 37,
"commandClassName": "Binary Switch",
"property": "currentValue",
"propertyName": "currentValue",
"ccVersion": 1,
"metadata": {
"type": "boolean",
"readable": true,
"writeable": false,
"label": "Current value"
},
"value": true
},
{
"endpoint": 4,
"commandClass": 37,
"commandClassName": "Binary Switch",
"property": "targetValue",
"propertyName": "targetValue",
"ccVersion": 1,
"metadata": {
"type": "boolean",
"readable": true,
"writeable": true,
"label": "Target value"
},
"value": true
}
],
"deviceClass": {
"basic": {
"key": 4,
"label": "Routing Slave"
},
"generic": {
"key": 33,
"label": "Multilevel Sensor"
},
"specific": {
"key": 1,
"label": "Routing Multilevel Sensor"
},
"mandatorySupportedCCs": [32, 49],
"mandatoryControlledCCs": []
},
"commandClasses": [
{
"id": 37,
"name": "Binary Switch",
"version": 1,
"isSecure": false
},
{
"id": 49,
"name": "Multilevel Sensor",
"version": 7,
"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": 122,
"name": "Firmware Update Meta Data",
"version": 3,
"isSecure": false
},
{
"id": 133,
"name": "Association",
"version": 2,
"isSecure": false
},
{
"id": 134,
"name": "Version",
"version": 2,
"isSecure": false
},
{
"id": 142,
"name": "Multi Channel Association",
"version": 3,
"isSecure": false
}
]
}