mirror of
https://github.com/home-assistant/core.git
synced 2025-07-28 15:47:12 +00:00
Merge pull request #47645 from home-assistant/rc
This commit is contained in:
commit
a8844ff24a
@ -46,7 +46,7 @@ homeassistant/components/arcam_fmj/* @elupus
|
||||
homeassistant/components/arduino/* @fabaff
|
||||
homeassistant/components/arest/* @fabaff
|
||||
homeassistant/components/arris_tg2492lg/* @vanbalken
|
||||
homeassistant/components/asuswrt/* @kennedyshead
|
||||
homeassistant/components/asuswrt/* @kennedyshead @ollo69
|
||||
homeassistant/components/atag/* @MatsNL
|
||||
homeassistant/components/aten_pe/* @mtdcr
|
||||
homeassistant/components/atome/* @baqs
|
||||
|
@ -393,7 +393,7 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator):
|
||||
),
|
||||
ATTR_FORECAST_TEMP: self._get_temperature_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_BEARING: self._get_wind_bearing_day(day),
|
||||
}
|
||||
@ -412,7 +412,7 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator):
|
||||
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_BEARING: self._get_wind_bearing(day, hour),
|
||||
}
|
||||
|
@ -128,7 +128,7 @@ class AsusWrtFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
|
||||
conf_protocol = user_input[CONF_PROTOCOL]
|
||||
if conf_protocol == PROTOCOL_TELNET:
|
||||
await api.connection.disconnect()
|
||||
api.connection.disconnect()
|
||||
return RESULT_SUCCESS
|
||||
|
||||
async def async_step_user(self, user_input=None):
|
||||
|
@ -4,5 +4,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/asuswrt",
|
||||
"requirements": ["aioasuswrt==1.3.1"],
|
||||
"codeowners": ["@kennedyshead"]
|
||||
"codeowners": ["@kennedyshead", "@ollo69"]
|
||||
}
|
||||
|
@ -205,7 +205,7 @@ class AsusWrtRouter:
|
||||
"""Close the connection."""
|
||||
if self._api is not None:
|
||||
if self._protocol == PROTOCOL_TELNET:
|
||||
await self._api.connection.disconnect()
|
||||
self._api.connection.disconnect()
|
||||
self._api = None
|
||||
|
||||
for func in self._on_close:
|
||||
|
@ -102,7 +102,7 @@ class BondEntity(Entity):
|
||||
|
||||
async def _async_update_if_bpup_not_alive(self, *_):
|
||||
"""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
|
||||
|
||||
if self._update_lock.locked():
|
||||
|
@ -3,7 +3,7 @@
|
||||
"name": "Home Assistant Frontend",
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"requirements": [
|
||||
"home-assistant-frontend==20210302.5"
|
||||
"home-assistant-frontend==20210302.6"
|
||||
],
|
||||
"dependencies": [
|
||||
"api",
|
||||
|
@ -616,7 +616,7 @@ class HomeKit:
|
||||
self._async_register_bridge(dev_reg)
|
||||
await self._async_start(bridged_states)
|
||||
_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
|
||||
|
||||
@callback
|
||||
|
@ -3,7 +3,7 @@
|
||||
"name": "HomeKit",
|
||||
"documentation": "https://www.home-assistant.io/integrations/homekit",
|
||||
"requirements": [
|
||||
"HAP-python==3.3.2",
|
||||
"HAP-python==3.4.0",
|
||||
"fnvhash==0.1.0",
|
||||
"PyQRCode==1.2.1",
|
||||
"base36==0.1.1",
|
||||
|
@ -27,7 +27,7 @@ from homeassistant.const import (
|
||||
STATE_ON,
|
||||
)
|
||||
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 .const import (
|
||||
@ -134,7 +134,7 @@ class Switch(HomeAccessory):
|
||||
self.async_call_service(self._domain, service, params)
|
||||
|
||||
if self.activate_only:
|
||||
call_later(self.hass, 1, self.reset_switch)
|
||||
async_call_later(self.hass, 1, self.reset_switch)
|
||||
|
||||
@callback
|
||||
def async_update_state(self, new_state):
|
||||
|
@ -80,6 +80,13 @@ class BaseHomeKitFan(HomeKitEntity, FanEntity):
|
||||
|
||||
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):
|
||||
"""Set the direction of the fan."""
|
||||
await self.async_put_characteristics(
|
||||
@ -110,7 +117,7 @@ class BaseHomeKitFan(HomeKitEntity, FanEntity):
|
||||
if not self.is_on:
|
||||
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
|
||||
|
||||
if characteristics:
|
||||
|
@ -1,8 +1,6 @@
|
||||
"""Support for INSTEON fans via PowerLinc Modem."""
|
||||
import math
|
||||
|
||||
from pyinsteon.constants import FanSpeed
|
||||
|
||||
from homeassistant.components.fan import (
|
||||
DOMAIN as FAN_DOMAIN,
|
||||
SUPPORT_SET_SPEED,
|
||||
@ -19,7 +17,7 @@ from .const import SIGNAL_ADD_ENTITIES
|
||||
from .insteon_entity import InsteonEntity
|
||||
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):
|
||||
@ -52,6 +50,11 @@ class InsteonFanEntity(InsteonEntity, FanEntity):
|
||||
"""Flag supported features."""
|
||||
return SUPPORT_SET_SPEED
|
||||
|
||||
@property
|
||||
def speed_count(self) -> int:
|
||||
"""Flag supported features."""
|
||||
return 3
|
||||
|
||||
async def async_turn_on(
|
||||
self,
|
||||
speed: str = None,
|
||||
@ -60,9 +63,7 @@ class InsteonFanEntity(InsteonEntity, FanEntity):
|
||||
**kwargs,
|
||||
) -> None:
|
||||
"""Turn on the fan."""
|
||||
if percentage is None:
|
||||
percentage = 50
|
||||
await self.async_set_percentage(percentage)
|
||||
await self.async_set_percentage(percentage or 67)
|
||||
|
||||
async def async_turn_off(self, **kwargs) -> None:
|
||||
"""Turn off the fan."""
|
||||
@ -71,7 +72,7 @@ class InsteonFanEntity(InsteonEntity, FanEntity):
|
||||
async def async_set_percentage(self, percentage: int) -> None:
|
||||
"""Set the speed percentage of the fan."""
|
||||
if percentage == 0:
|
||||
await self._insteon_device.async_fan_off()
|
||||
else:
|
||||
on_level = math.ceil(percentage_to_ranged_value(SPEED_RANGE, percentage))
|
||||
await self._insteon_device.async_fan_on(on_level=on_level)
|
||||
await self.async_turn_off()
|
||||
return
|
||||
on_level = math.ceil(percentage_to_ranged_value(SPEED_RANGE, percentage))
|
||||
await self._insteon_device.async_on(group=2, on_level=on_level)
|
||||
|
@ -35,9 +35,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
hub = LitterRobotHub(self.hass, user_input)
|
||||
try:
|
||||
await hub.login()
|
||||
return self.async_create_entry(
|
||||
title=user_input[CONF_USERNAME], data=user_input
|
||||
)
|
||||
except LitterRobotLoginException:
|
||||
errors["base"] = "invalid_auth"
|
||||
except LitterRobotException:
|
||||
@ -46,6 +43,11 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
|
||||
if not errors:
|
||||
return self.async_create_entry(
|
||||
title=user_input[CONF_USERNAME], data=user_input
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
|
||||
)
|
||||
|
@ -4,7 +4,7 @@ import logging
|
||||
from types import MethodType
|
||||
from typing import Any, Optional
|
||||
|
||||
from pylitterbot import Account, Robot
|
||||
import pylitterbot
|
||||
from pylitterbot.exceptions import LitterRobotException, LitterRobotLoginException
|
||||
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||
@ -49,7 +49,7 @@ class LitterRobotHub:
|
||||
async def login(self, load_robots: bool = False):
|
||||
"""Login to Litter-Robot."""
|
||||
self.logged_in = False
|
||||
self.account = Account()
|
||||
self.account = pylitterbot.Account()
|
||||
try:
|
||||
await self.account.connect(
|
||||
username=self._data[CONF_USERNAME],
|
||||
@ -69,11 +69,11 @@ class LitterRobotHub:
|
||||
class LitterRobotEntity(CoordinatorEntity):
|
||||
"""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."""
|
||||
super().__init__(hub.coordinator)
|
||||
self.robot = robot
|
||||
self.entity_type = entity_type if entity_type else ""
|
||||
self.entity_type = entity_type
|
||||
self.hub = hub
|
||||
|
||||
@property
|
||||
@ -89,22 +89,21 @@ class LitterRobotEntity(CoordinatorEntity):
|
||||
@property
|
||||
def device_info(self):
|
||||
"""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 {
|
||||
"identifiers": {(DOMAIN, self.robot.serial)},
|
||||
"name": self.robot.name,
|
||||
"manufacturer": "Litter-Robot",
|
||||
"model": model,
|
||||
"model": self.robot.model,
|
||||
}
|
||||
|
||||
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."""
|
||||
|
||||
async def async_call_later_callback(*_) -> None:
|
||||
await self.hub.coordinator.async_request_refresh()
|
||||
|
||||
await action(*args)
|
||||
async_call_later(
|
||||
self.hass, REFRESH_WAIT_TIME, self.hub.coordinator.async_request_refresh
|
||||
)
|
||||
async_call_later(self.hass, REFRESH_WAIT_TIME, async_call_later_callback)
|
||||
|
||||
@staticmethod
|
||||
def parse_time_at_default_timezone(time_str: str) -> Optional[time]:
|
||||
|
@ -3,6 +3,6 @@
|
||||
"name": "Litter-Robot",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/litterrobot",
|
||||
"requirements": ["pylitterbot==2021.2.5"],
|
||||
"requirements": ["pylitterbot==2021.2.8"],
|
||||
"codeowners": ["@natekspencer"]
|
||||
}
|
||||
|
@ -1,32 +1,44 @@
|
||||
"""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 .const import DOMAIN
|
||||
from .hub import LitterRobotEntity
|
||||
|
||||
WASTE_DRAWER = "Waste Drawer"
|
||||
from .hub import LitterRobotEntity, LitterRobotHub
|
||||
|
||||
|
||||
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:
|
||||
entities.append(LitterRobotSensor(robot, WASTE_DRAWER, hub))
|
||||
|
||||
if entities:
|
||||
async_add_entities(entities, True)
|
||||
def icon_for_gauge_level(gauge_level: Optional[int] = None, offset: int = 0) -> str:
|
||||
"""Return a gauge icon valid identifier."""
|
||||
if gauge_level is None or gauge_level <= 0 + offset:
|
||||
return "mdi:gauge-empty"
|
||||
if gauge_level > 70 + offset:
|
||||
return "mdi:gauge-full"
|
||||
if gauge_level > 30 + offset:
|
||||
return "mdi:gauge"
|
||||
return "mdi:gauge-low"
|
||||
|
||||
|
||||
class LitterRobotSensor(LitterRobotEntity, Entity):
|
||||
"""Litter-Robot sensors."""
|
||||
class LitterRobotPropertySensor(LitterRobotEntity, Entity):
|
||||
"""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
|
||||
def state(self):
|
||||
"""Return the state."""
|
||||
return self.robot.waste_drawer_gauge
|
||||
return getattr(self.robot, self.sensor_attribute)
|
||||
|
||||
|
||||
class LitterRobotWasteSensor(LitterRobotPropertySensor, Entity):
|
||||
"""Litter-Robot sensors."""
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
@ -36,19 +48,40 @@ class LitterRobotSensor(LitterRobotEntity, Entity):
|
||||
@property
|
||||
def icon(self):
|
||||
"""Return the icon to use in the frontend, if any."""
|
||||
if self.robot.waste_drawer_gauge <= 10:
|
||||
return "mdi:gauge-empty"
|
||||
if self.robot.waste_drawer_gauge < 50:
|
||||
return "mdi:gauge-low"
|
||||
if self.robot.waste_drawer_gauge <= 90:
|
||||
return "mdi:gauge"
|
||||
return "mdi:gauge-full"
|
||||
return icon_for_gauge_level(self.state, 10)
|
||||
|
||||
|
||||
class LitterRobotSleepTimeSensor(LitterRobotPropertySensor, Entity):
|
||||
"""Litter-Robot sleep time sensors."""
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return device specific state attributes."""
|
||||
return {
|
||||
"cycle_count": self.robot.cycle_count,
|
||||
"cycle_capacity": self.robot.cycle_capacity,
|
||||
"cycles_after_drawer_full": self.robot.cycles_after_drawer_full,
|
||||
}
|
||||
def state(self):
|
||||
"""Return the state."""
|
||||
if self.robot.sleep_mode_active:
|
||||
return super().state.isoformat()
|
||||
return None
|
||||
|
||||
@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)
|
||||
|
@ -1,11 +1,11 @@
|
||||
"""Support for Litter-Robot switches."""
|
||||
from homeassistant.helpers.entity import ToggleEntity
|
||||
from homeassistant.components.switch import SwitchEntity
|
||||
|
||||
from .const import DOMAIN
|
||||
from .hub import LitterRobotEntity
|
||||
|
||||
|
||||
class LitterRobotNightLightModeSwitch(LitterRobotEntity, ToggleEntity):
|
||||
class LitterRobotNightLightModeSwitch(LitterRobotEntity, SwitchEntity):
|
||||
"""Litter-Robot Night Light Mode Switch."""
|
||||
|
||||
@property
|
||||
@ -27,7 +27,7 @@ class LitterRobotNightLightModeSwitch(LitterRobotEntity, ToggleEntity):
|
||||
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."""
|
||||
|
||||
@property
|
||||
|
@ -14,7 +14,6 @@ from homeassistant.components.vacuum import (
|
||||
VacuumEntity,
|
||||
)
|
||||
from homeassistant.const import STATE_OFF
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
from .const import DOMAIN
|
||||
from .hub import LitterRobotEntity
|
||||
@ -54,27 +53,22 @@ class LitterRobotCleaner(LitterRobotEntity, VacuumEntity):
|
||||
def state(self):
|
||||
"""Return the state of the cleaner."""
|
||||
switcher = {
|
||||
Robot.UnitStatus.CCP: STATE_CLEANING,
|
||||
Robot.UnitStatus.EC: STATE_CLEANING,
|
||||
Robot.UnitStatus.CCC: STATE_DOCKED,
|
||||
Robot.UnitStatus.CST: STATE_DOCKED,
|
||||
Robot.UnitStatus.DF1: STATE_DOCKED,
|
||||
Robot.UnitStatus.DF2: STATE_DOCKED,
|
||||
Robot.UnitStatus.RDY: STATE_DOCKED,
|
||||
Robot.UnitStatus.CLEAN_CYCLE: STATE_CLEANING,
|
||||
Robot.UnitStatus.EMPTY_CYCLE: STATE_CLEANING,
|
||||
Robot.UnitStatus.CLEAN_CYCLE_COMPLETE: STATE_DOCKED,
|
||||
Robot.UnitStatus.CAT_SENSOR_TIMING: STATE_DOCKED,
|
||||
Robot.UnitStatus.DRAWER_FULL_1: STATE_DOCKED,
|
||||
Robot.UnitStatus.DRAWER_FULL_2: STATE_DOCKED,
|
||||
Robot.UnitStatus.READY: STATE_DOCKED,
|
||||
Robot.UnitStatus.OFF: STATE_OFF,
|
||||
}
|
||||
|
||||
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
|
||||
def status(self):
|
||||
"""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):
|
||||
"""Turn the cleaner on, starting a clean cycle."""
|
||||
@ -119,22 +113,11 @@ class LitterRobotCleaner(LitterRobotEntity, VacuumEntity):
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""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 {
|
||||
"clean_cycle_wait_time_minutes": self.robot.clean_cycle_wait_time_minutes,
|
||||
"is_sleeping": self.robot.is_sleeping,
|
||||
"sleep_mode_start_time": sleep_mode_start_time,
|
||||
"sleep_mode_end_time": sleep_mode_end_time,
|
||||
"sleep_mode_active": self.robot.sleep_mode_active,
|
||||
"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,
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
"""Connect to a MySensors gateway via pymysensors API."""
|
||||
import asyncio
|
||||
from functools import partial
|
||||
import logging
|
||||
from typing import Callable, Dict, List, Optional, Tuple, Type, Union
|
||||
|
||||
@ -7,11 +8,15 @@ from mysensors import BaseAsyncGateway
|
||||
import voluptuous as vol
|
||||
|
||||
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.notify import DOMAIN as NOTIFY_DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_OPTIMISTIC
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
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 .const import (
|
||||
@ -28,21 +33,24 @@ from .const import (
|
||||
CONF_TOPIC_OUT_PREFIX,
|
||||
CONF_VERSION,
|
||||
DOMAIN,
|
||||
MYSENSORS_DISCOVERY,
|
||||
MYSENSORS_GATEWAYS,
|
||||
MYSENSORS_ON_UNLOAD,
|
||||
SUPPORTED_PLATFORMS_WITH_ENTRY_SUPPORT,
|
||||
DevId,
|
||||
GatewayId,
|
||||
SensorType,
|
||||
)
|
||||
from .device import MySensorsDevice, MySensorsEntity, get_mysensors_devices
|
||||
from .gateway import finish_setup, get_mysensors_gateway, gw_stop, setup_gateway
|
||||
from .helpers import on_unload
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_DEBUG = "debug"
|
||||
CONF_NODE_NAME = "name"
|
||||
|
||||
DATA_HASS_CONFIG = "hass_config"
|
||||
|
||||
DEFAULT_BAUD_RATE = 115200
|
||||
DEFAULT_TCP_PORT = 5003
|
||||
DEFAULT_VERSION = "1.4"
|
||||
@ -134,6 +142,8 @@ CONFIG_SCHEMA = vol.Schema(
|
||||
|
||||
async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool:
|
||||
"""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)):
|
||||
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)
|
||||
return False
|
||||
|
||||
if DOMAIN not in hass.data:
|
||||
hass.data[DOMAIN] = {}
|
||||
|
||||
if MYSENSORS_GATEWAYS not in hass.data[DOMAIN]:
|
||||
hass.data[DOMAIN][MYSENSORS_GATEWAYS] = {}
|
||||
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(
|
||||
*[
|
||||
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]:
|
||||
fnct()
|
||||
|
||||
hass.data[DOMAIN].pop(key)
|
||||
|
||||
del hass.data[DOMAIN][MYSENSORS_GATEWAYS][entry.entry_id]
|
||||
|
||||
await gw_stop(hass, entry, gateway)
|
||||
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
|
||||
def setup_mysensors_platform(
|
||||
hass,
|
||||
hass: HomeAssistant,
|
||||
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_args: Optional[
|
||||
Tuple
|
||||
] = 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]]:
|
||||
"""Set up a MySensors platform.
|
||||
|
||||
@ -264,11 +279,6 @@ def setup_mysensors_platform(
|
||||
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
|
||||
"""
|
||||
# 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:
|
||||
device_args = ()
|
||||
new_devices: List[MySensorsDevice] = []
|
||||
|
@ -12,6 +12,9 @@ async def async_setup_scanner(
|
||||
hass: HomeAssistantType, config, async_see, discovery_info=None
|
||||
):
|
||||
"""Set up the MySensors device scanner."""
|
||||
if not discovery_info:
|
||||
return False
|
||||
|
||||
new_devices = mysensors.setup_mysensors_platform(
|
||||
hass,
|
||||
DOMAIN,
|
||||
|
@ -31,7 +31,12 @@ from .const import (
|
||||
GatewayId,
|
||||
)
|
||||
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__)
|
||||
|
||||
@ -260,8 +265,8 @@ async def _discover_persistent_devices(
|
||||
|
||||
async def gw_stop(hass, entry: ConfigEntry, gateway: BaseAsyncGateway):
|
||||
"""Stop the gateway."""
|
||||
connect_task = hass.data[DOMAIN].get(
|
||||
MYSENSORS_GATEWAY_START_TASK.format(entry.entry_id)
|
||||
connect_task = hass.data[DOMAIN].pop(
|
||||
MYSENSORS_GATEWAY_START_TASK.format(entry.entry_id), None
|
||||
)
|
||||
if connect_task is not None and not connect_task.done():
|
||||
connect_task.cancel()
|
||||
@ -288,7 +293,12 @@ async def _gw_start(
|
||||
async def stop_this_gw(_: Event):
|
||||
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:
|
||||
# Gatways connected via mqtt doesn't send gateway ready message.
|
||||
return
|
||||
|
@ -2,16 +2,18 @@
|
||||
from collections import defaultdict
|
||||
from enum import IntEnum
|
||||
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.sensor import ChildSensor
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_NAME
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
from homeassistant.helpers.typing import HomeAssistantType
|
||||
from homeassistant.util.decorator import Registry
|
||||
|
||||
from .const import (
|
||||
@ -20,6 +22,7 @@ from .const import (
|
||||
DOMAIN,
|
||||
FLAT_PLATFORM_TYPES,
|
||||
MYSENSORS_DISCOVERY,
|
||||
MYSENSORS_ON_UNLOAD,
|
||||
TYPE_TO_PLATFORMS,
|
||||
DevId,
|
||||
GatewayId,
|
||||
@ -31,9 +34,26 @@ _LOGGER = logging.getLogger(__name__)
|
||||
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
|
||||
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:
|
||||
"""Discover a MySensors platform."""
|
||||
_LOGGER.debug("Discovering platform %s with devIds: %s", platform, new_devices)
|
||||
|
@ -2,15 +2,8 @@
|
||||
"domain": "mysensors",
|
||||
"name": "MySensors",
|
||||
"documentation": "https://www.home-assistant.io/integrations/mysensors",
|
||||
"requirements": [
|
||||
"pymysensors==0.20.1"
|
||||
],
|
||||
"after_dependencies": [
|
||||
"mqtt"
|
||||
],
|
||||
"codeowners": [
|
||||
"@MartinHjelmare",
|
||||
"@functionpointer"
|
||||
],
|
||||
"requirements": ["pymysensors==0.21.0"],
|
||||
"after_dependencies": ["mqtt"],
|
||||
"codeowners": ["@MartinHjelmare", "@functionpointer"],
|
||||
"config_flow": true
|
||||
}
|
||||
|
@ -5,6 +5,9 @@ from homeassistant.components.notify import ATTR_TARGET, DOMAIN, BaseNotificatio
|
||||
|
||||
async def async_get_service(hass, config, discovery_info=None):
|
||||
"""Get the MySensors notification service."""
|
||||
if not discovery_info:
|
||||
return None
|
||||
|
||||
new_devices = mysensors.setup_mysensors_platform(
|
||||
hass, DOMAIN, discovery_info, MySensorsNotificationDevice
|
||||
)
|
||||
|
@ -139,7 +139,9 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator):
|
||||
|
||||
def _convert_forecast(self, entry):
|
||||
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(
|
||||
entry.rain, entry.snow
|
||||
),
|
||||
|
@ -7,8 +7,7 @@
|
||||
"python-openzwave-mqtt[mqtt-client]==1.4.0"
|
||||
],
|
||||
"after_dependencies": [
|
||||
"mqtt",
|
||||
"zwave"
|
||||
"mqtt"
|
||||
],
|
||||
"codeowners": [
|
||||
"@cgarwood",
|
||||
|
@ -177,9 +177,9 @@ def get_device_wrapper(hass: HomeAssistant, device_id: str):
|
||||
return None
|
||||
|
||||
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 None
|
||||
|
@ -646,9 +646,12 @@ class SonosEntity(MediaPlayerEntity):
|
||||
update_position = new_status != self._status
|
||||
self._status = new_status
|
||||
|
||||
track_uri = variables["current_track_uri"] if variables else None
|
||||
|
||||
music_source = self.soco.music_source_from_uri(track_uri)
|
||||
if variables:
|
||||
track_uri = variables["current_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:
|
||||
self.update_media_linein(SOURCE_TV)
|
||||
|
@ -523,7 +523,6 @@ class TemplateFan(TemplateEntity, FanEntity):
|
||||
speed = str(speed)
|
||||
|
||||
if speed in self._speed_list:
|
||||
self._state = STATE_OFF if speed == SPEED_OFF else STATE_ON
|
||||
self._speed = speed
|
||||
self._percentage = self.speed_to_percentage(speed)
|
||||
self._preset_mode = speed if speed in self.preset_modes else None
|
||||
@ -552,7 +551,6 @@ class TemplateFan(TemplateEntity, FanEntity):
|
||||
return
|
||||
|
||||
if 0 <= percentage <= 100:
|
||||
self._state = STATE_OFF if percentage == 0 else STATE_ON
|
||||
self._percentage = percentage
|
||||
if self._speed_list:
|
||||
self._speed = self.percentage_to_speed(percentage)
|
||||
@ -569,7 +567,6 @@ class TemplateFan(TemplateEntity, FanEntity):
|
||||
preset_mode = str(preset_mode)
|
||||
|
||||
if preset_mode in self.preset_modes:
|
||||
self._state = STATE_ON
|
||||
self._speed = preset_mode
|
||||
self._percentage = None
|
||||
self._preset_mode = preset_mode
|
||||
|
@ -162,6 +162,15 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity):
|
||||
add_to_watched_value_ids=True,
|
||||
)
|
||||
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:
|
||||
"""Optionally return a ZwaveValue for a setpoint."""
|
||||
@ -259,7 +268,7 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity):
|
||||
return None
|
||||
try:
|
||||
temp = self._setpoint_value(self._current_mode_setpoint_enums[0])
|
||||
except ValueError:
|
||||
except (IndexError, ValueError):
|
||||
return None
|
||||
return temp.value if temp else None
|
||||
|
||||
@ -271,7 +280,7 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity):
|
||||
return None
|
||||
try:
|
||||
temp = self._setpoint_value(self._current_mode_setpoint_enums[1])
|
||||
except ValueError:
|
||||
except (IndexError, ValueError):
|
||||
return None
|
||||
return temp.value if temp else None
|
||||
|
||||
@ -335,14 +344,7 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity):
|
||||
@property
|
||||
def supported_features(self) -> int:
|
||||
"""Return the list of supported features."""
|
||||
support = SUPPORT_PRESET_MODE
|
||||
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
|
||||
return self._supported_features
|
||||
|
||||
async def async_set_fan_mode(self, fan_mode: str) -> None:
|
||||
"""Set new target fan mode."""
|
||||
|
@ -105,7 +105,11 @@ class ZWaveBaseEntity(Entity):
|
||||
"""Generate entity name."""
|
||||
if additional_info is None:
|
||||
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:
|
||||
value_name = (
|
||||
alternate_value_name
|
||||
|
@ -1,7 +1,7 @@
|
||||
"""Constants used by Home Assistant components."""
|
||||
MAJOR_VERSION = 2021
|
||||
MINOR_VERSION = 3
|
||||
PATCH_VERSION = "2"
|
||||
PATCH_VERSION = "3"
|
||||
__short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||
__version__ = f"{__short_version__}.{PATCH_VERSION}"
|
||||
REQUIRED_PYTHON_VER = (3, 8, 0)
|
||||
|
@ -15,7 +15,7 @@ defusedxml==0.6.0
|
||||
distro==1.5.0
|
||||
emoji==1.2.0
|
||||
hass-nabucasa==0.41.0
|
||||
home-assistant-frontend==20210302.5
|
||||
home-assistant-frontend==20210302.6
|
||||
httpx==0.16.1
|
||||
jinja2>=2.11.3
|
||||
netdisco==2.8.2
|
||||
|
@ -17,7 +17,7 @@ Adafruit-SHT31==1.0.2
|
||||
# Adafruit_BBIO==1.1.1
|
||||
|
||||
# homeassistant.components.homekit
|
||||
HAP-python==3.3.2
|
||||
HAP-python==3.4.0
|
||||
|
||||
# homeassistant.components.mastodon
|
||||
Mastodon.py==1.5.1
|
||||
@ -763,7 +763,7 @@ hole==0.5.1
|
||||
holidays==0.10.5.2
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20210302.5
|
||||
home-assistant-frontend==20210302.6
|
||||
|
||||
# homeassistant.components.zwave
|
||||
homeassistant-pyozw==0.1.10
|
||||
@ -1504,7 +1504,7 @@ pylibrespot-java==0.1.0
|
||||
pylitejet==0.3.0
|
||||
|
||||
# homeassistant.components.litterrobot
|
||||
pylitterbot==2021.2.5
|
||||
pylitterbot==2021.2.8
|
||||
|
||||
# homeassistant.components.loopenergy
|
||||
pyloopenergy==0.2.1
|
||||
@ -1555,7 +1555,7 @@ pymusiccast==0.1.6
|
||||
pymyq==3.0.4
|
||||
|
||||
# homeassistant.components.mysensors
|
||||
pymysensors==0.20.1
|
||||
pymysensors==0.21.0
|
||||
|
||||
# homeassistant.components.nanoleaf
|
||||
pynanoleaf==0.0.5
|
||||
|
@ -7,7 +7,7 @@
|
||||
AEMET-OpenData==0.1.8
|
||||
|
||||
# homeassistant.components.homekit
|
||||
HAP-python==3.3.2
|
||||
HAP-python==3.4.0
|
||||
|
||||
# homeassistant.components.flick_electric
|
||||
PyFlick==0.0.2
|
||||
@ -412,7 +412,7 @@ hole==0.5.1
|
||||
holidays==0.10.5.2
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20210302.5
|
||||
home-assistant-frontend==20210302.6
|
||||
|
||||
# homeassistant.components.zwave
|
||||
homeassistant-pyozw==0.1.10
|
||||
@ -794,7 +794,7 @@ pylibrespot-java==0.1.0
|
||||
pylitejet==0.3.0
|
||||
|
||||
# homeassistant.components.litterrobot
|
||||
pylitterbot==2021.2.5
|
||||
pylitterbot==2021.2.8
|
||||
|
||||
# homeassistant.components.lutron_caseta
|
||||
pylutron-caseta==0.9.0
|
||||
@ -827,7 +827,7 @@ pymonoprice==0.3
|
||||
pymyq==3.0.4
|
||||
|
||||
# homeassistant.components.mysensors
|
||||
pymysensors==0.20.1
|
||||
pymysensors==0.21.0
|
||||
|
||||
# homeassistant.components.nuki
|
||||
pynuki==1.3.8
|
||||
|
@ -147,6 +147,8 @@ IGNORE_VIOLATIONS = {
|
||||
# Demo
|
||||
("demo", "manual"),
|
||||
("demo", "openalpr_local"),
|
||||
# Migration wizard from zwave to ozw.
|
||||
"ozw",
|
||||
# This should become a helper method that integrations can submit data to
|
||||
("websocket_api", "lovelace"),
|
||||
("websocket_api", "shopping_list"),
|
||||
|
@ -37,7 +37,9 @@ async def test_aemet_forecast_create_sensors(hass):
|
||||
assert state.state == "-4"
|
||||
|
||||
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")
|
||||
assert state.state == "45.0"
|
||||
|
@ -51,8 +51,9 @@ async def test_aemet_weather(hass):
|
||||
assert forecast.get(ATTR_FORECAST_PRECIPITATION_PROBABILITY) == 30
|
||||
assert forecast.get(ATTR_FORECAST_TEMP) == 4
|
||||
assert forecast.get(ATTR_FORECAST_TEMP_LOW) == -4
|
||||
assert forecast.get(ATTR_FORECAST_TIME) == dt_util.parse_datetime(
|
||||
"2021-01-10 00:00:00+00:00"
|
||||
assert (
|
||||
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_SPEED) == 20
|
||||
|
@ -1,6 +1,6 @@
|
||||
"""Tests for the AsusWrt config flow."""
|
||||
from socket import gaierror
|
||||
from unittest.mock import AsyncMock, patch
|
||||
from unittest.mock import AsyncMock, Mock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
@ -46,7 +46,7 @@ def mock_controller_connect():
|
||||
with patch("homeassistant.components.asuswrt.router.AsusWrt") as service_mock:
|
||||
service_mock.return_value.connection.async_connect = AsyncMock()
|
||||
service_mock.return_value.is_connected = True
|
||||
service_mock.return_value.connection.disconnect = AsyncMock()
|
||||
service_mock.return_value.connection.disconnect = Mock()
|
||||
yield service_mock
|
||||
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
"""Tests for the AsusWrt sensor."""
|
||||
from datetime import timedelta
|
||||
from unittest.mock import AsyncMock, patch
|
||||
from unittest.mock import AsyncMock, Mock, patch
|
||||
|
||||
from aioasuswrt.asuswrt import Device
|
||||
import pytest
|
||||
@ -49,7 +49,7 @@ def mock_controller_connect():
|
||||
with patch("homeassistant.components.asuswrt.router.AsusWrt") as service_mock:
|
||||
service_mock.return_value.connection.async_connect = AsyncMock()
|
||||
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(
|
||||
return_value=MOCK_DEVICES
|
||||
)
|
||||
|
169
tests/components/bond/test_entity.py
Normal file
169
tests/components/bond/test_entity.py
Normal 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
|
@ -493,7 +493,7 @@ async def test_homekit_start(hass, hk_driver, device_reg):
|
||||
) as mock_setup_msg, patch(
|
||||
"pyhap.accessory_driver.AccessoryDriver.add_accessory"
|
||||
) as hk_driver_add_acc, patch(
|
||||
"pyhap.accessory_driver.AccessoryDriver.start_service"
|
||||
"pyhap.accessory_driver.AccessoryDriver.async_start"
|
||||
) as hk_driver_start:
|
||||
await homekit.async_start()
|
||||
|
||||
@ -528,7 +528,7 @@ async def test_homekit_start(hass, hk_driver, device_reg):
|
||||
) as mock_setup_msg, patch(
|
||||
"pyhap.accessory_driver.AccessoryDriver.add_accessory"
|
||||
) as hk_driver_add_acc, patch(
|
||||
"pyhap.accessory_driver.AccessoryDriver.start_service"
|
||||
"pyhap.accessory_driver.AccessoryDriver.async_start"
|
||||
) as hk_driver_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(
|
||||
"pyhap.accessory_driver.AccessoryDriver.add_accessory",
|
||||
) as hk_driver_add_acc, patch(
|
||||
"pyhap.accessory_driver.AccessoryDriver.start_service"
|
||||
"pyhap.accessory_driver.AccessoryDriver.async_start"
|
||||
) as hk_driver_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(
|
||||
"pyhap.accessory_driver.AccessoryDriver.config_changed"
|
||||
) as hk_driver_config_changed, patch(
|
||||
"pyhap.accessory_driver.AccessoryDriver.start_service"
|
||||
"pyhap.accessory_driver.AccessoryDriver.async_start"
|
||||
):
|
||||
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.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"
|
||||
), patch(f"{PATH_HOMEKIT}.show_setup_message"), patch(
|
||||
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(
|
||||
f"{PATH_HOMEKIT}.show_setup_message"
|
||||
), 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 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(
|
||||
f"{PATH_HOMEKIT}.show_setup_message"
|
||||
), 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 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": {}})
|
||||
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"
|
||||
):
|
||||
entry.add_to_hass(hass)
|
||||
@ -963,7 +963,7 @@ async def test_homekit_ignored_missing_devices(
|
||||
with patch.object(homekit.bridge, "add_accessory"), patch(
|
||||
f"{PATH_HOMEKIT}.show_setup_message"
|
||||
), 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 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(
|
||||
f"{PATH_HOMEKIT}.show_setup_message"
|
||||
), 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 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(
|
||||
f"{PATH_HOMEKIT}.show_setup_message"
|
||||
), 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 hass.async_block_till_done()
|
||||
@ -1153,7 +1153,7 @@ async def test_reload(hass, mock_zeroconf):
|
||||
), patch(
|
||||
f"{PATH_HOMEKIT}.get_accessory"
|
||||
), patch(
|
||||
"pyhap.accessory_driver.AccessoryDriver.start_service"
|
||||
"pyhap.accessory_driver.AccessoryDriver.async_start"
|
||||
):
|
||||
mock_homekit2.return_value = homekit = Mock()
|
||||
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(
|
||||
"pyhap.accessory_driver.AccessoryDriver.add_accessory"
|
||||
), 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:
|
||||
await homekit.async_start()
|
||||
|
||||
|
@ -11,6 +11,13 @@ import homeassistant.util.dt as dt_util
|
||||
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
|
||||
def utcnow(request):
|
||||
"""Freeze time at a known point."""
|
||||
|
@ -50,6 +50,38 @@ def create_fanv2_service(accessory):
|
||||
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):
|
||||
"""Test that we can read the state of a HomeKit fan accessory."""
|
||||
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
|
||||
|
||||
|
||||
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):
|
||||
"""Test that we can turn a fan off."""
|
||||
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()
|
||||
assert state.attributes["speed"] == "high"
|
||||
assert state.attributes["percentage"] == 100
|
||||
assert state.attributes["percentage_step"] == 1.0
|
||||
|
||||
helper.characteristics[V1_ROTATION_SPEED].value = 50
|
||||
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_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):
|
||||
"""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
|
||||
|
||||
|
||||
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):
|
||||
"""Test that we can read a fans oscillation."""
|
||||
helper = await setup_test_component(hass, create_fanv2_service)
|
||||
|
@ -1,45 +1,59 @@
|
||||
"""Configure pytest for Litter-Robot tests."""
|
||||
from typing import Optional
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pylitterbot
|
||||
from pylitterbot import Robot
|
||||
import pytest
|
||||
|
||||
from homeassistant.components import litterrobot
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
|
||||
from .common import CONFIG, ROBOT_DATA
|
||||
|
||||
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."""
|
||||
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
|
||||
if not (
|
||||
unit_status_code
|
||||
and Robot.UnitStatus(unit_status_code) != Robot.UnitStatus.UNKNOWN
|
||||
):
|
||||
unit_status_code = ROBOT_DATA["unitStatus"]
|
||||
|
||||
with patch.dict(ROBOT_DATA, {"unitStatus": unit_status_code}):
|
||||
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 mock_hub(hass):
|
||||
"""Mock a Litter-Robot hub."""
|
||||
hub = MagicMock(
|
||||
hass=hass,
|
||||
account=MagicMock(),
|
||||
logged_in=True,
|
||||
coordinator=MagicMock(spec=DataUpdateCoordinator),
|
||||
spec=litterrobot.LitterRobotHub,
|
||||
)
|
||||
hub.coordinator.last_update_success = True
|
||||
hub.account.robots = [create_mock_robot(hass)]
|
||||
return hub
|
||||
def create_mock_account(unit_status_code: Optional[str] = None):
|
||||
"""Create a mock Litter-Robot account."""
|
||||
account = MagicMock(spec=pylitterbot.Account)
|
||||
account.connect = AsyncMock()
|
||||
account.refresh_robots = AsyncMock()
|
||||
account.robots = [create_mock_robot(unit_status_code)]
|
||||
return account
|
||||
|
||||
|
||||
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."""
|
||||
entry = MockConfigEntry(
|
||||
domain=litterrobot.DOMAIN,
|
||||
@ -47,9 +61,11 @@ async def setup_hub(hass, mock_hub, platform_domain):
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.litterrobot.LitterRobotHub",
|
||||
return_value=mock_hub,
|
||||
with patch("pylitterbot.Account", return_value=mock_account), patch(
|
||||
"homeassistant.components.litterrobot.PLATFORMS",
|
||||
[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()
|
||||
|
||||
return entry
|
||||
|
@ -4,11 +4,14 @@ from unittest.mock import patch
|
||||
from pylitterbot.exceptions import LitterRobotException, LitterRobotLoginException
|
||||
|
||||
from homeassistant import config_entries, setup
|
||||
from homeassistant.components import litterrobot
|
||||
|
||||
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."""
|
||||
await setup.async_setup_component(hass, "persistent_notification", {})
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
@ -17,10 +20,7 @@ async def test_form(hass):
|
||||
assert result["type"] == "form"
|
||||
assert result["errors"] == {}
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.litterrobot.config_flow.LitterRobotHub.login",
|
||||
return_value=True,
|
||||
), patch(
|
||||
with patch("pylitterbot.Account", return_value=mock_account), patch(
|
||||
"homeassistant.components.litterrobot.async_setup", return_value=True
|
||||
) as mock_setup, patch(
|
||||
"homeassistant.components.litterrobot.async_setup_entry",
|
||||
@ -38,6 +38,23 @@ async def test_form(hass):
|
||||
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):
|
||||
"""Test we handle invalid auth."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
@ -45,7 +62,7 @@ async def test_form_invalid_auth(hass):
|
||||
)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.litterrobot.config_flow.LitterRobotHub.login",
|
||||
"pylitterbot.Account.connect",
|
||||
side_effect=LitterRobotLoginException,
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
@ -63,7 +80,7 @@ async def test_form_cannot_connect(hass):
|
||||
)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.litterrobot.config_flow.LitterRobotHub.login",
|
||||
"pylitterbot.Account.connect",
|
||||
side_effect=LitterRobotException,
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
@ -81,7 +98,7 @@ async def test_form_unknown_error(hass):
|
||||
)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.litterrobot.config_flow.LitterRobotHub.login",
|
||||
"pylitterbot.Account.connect",
|
||||
side_effect=Exception,
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
|
@ -1,20 +1,48 @@
|
||||
"""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.setup import async_setup_component
|
||||
from homeassistant.config_entries import (
|
||||
ENTRY_STATE_SETUP_ERROR,
|
||||
ENTRY_STATE_SETUP_RETRY,
|
||||
)
|
||||
|
||||
from .common import CONFIG
|
||||
from .conftest import setup_integration
|
||||
|
||||
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."""
|
||||
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(
|
||||
domain=litterrobot.DOMAIN,
|
||||
data=CONFIG[litterrobot.DOMAIN],
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
assert await async_setup_component(hass, litterrobot.DOMAIN, {}) is True
|
||||
assert await litterrobot.async_unload_entry(hass, entry)
|
||||
assert hass.data[litterrobot.DOMAIN] == {}
|
||||
with patch(
|
||||
"pylitterbot.Account.connect",
|
||||
side_effect=side_effect,
|
||||
):
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
assert entry.state == expected_state
|
||||
|
@ -1,20 +1,57 @@
|
||||
"""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.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):
|
||||
"""Tests the sensor entity was set up."""
|
||||
await setup_hub(hass, mock_hub, PLATFORM_DOMAIN)
|
||||
async def test_waste_drawer_sensor(hass, mock_account):
|
||||
"""Tests the waste drawer sensor entity was set up."""
|
||||
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.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
|
||||
|
||||
|
||||
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
|
||||
|
@ -12,7 +12,7 @@ from homeassistant.components.switch import (
|
||||
from homeassistant.const import ATTR_ENTITY_ID, STATE_ON
|
||||
from homeassistant.util.dt import utcnow
|
||||
|
||||
from .conftest import setup_hub
|
||||
from .conftest import setup_integration
|
||||
|
||||
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"
|
||||
|
||||
|
||||
async def test_switch(hass, mock_hub):
|
||||
async def test_switch(hass, mock_account):
|
||||
"""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)
|
||||
assert switch
|
||||
@ -36,9 +36,9 @@ async def test_switch(hass, mock_hub):
|
||||
(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."""
|
||||
await setup_hub(hass, mock_hub, PLATFORM_DOMAIN)
|
||||
await setup_integration(hass, mock_account, PLATFORM_DOMAIN)
|
||||
|
||||
switch = hass.states.get(entity_id)
|
||||
assert switch
|
||||
@ -48,12 +48,14 @@ async def test_on_off_commands(hass, mock_hub, entity_id, robot_command):
|
||||
count = 0
|
||||
for service in [SERVICE_TURN_ON, SERVICE_TURN_OFF]:
|
||||
count += 1
|
||||
|
||||
await hass.services.async_call(
|
||||
PLATFORM_DOMAIN,
|
||||
service,
|
||||
data,
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
future = utcnow() + timedelta(seconds=REFRESH_WAIT_TIME)
|
||||
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
|
||||
|
@ -12,20 +12,21 @@ from homeassistant.components.vacuum import (
|
||||
SERVICE_TURN_OFF,
|
||||
SERVICE_TURN_ON,
|
||||
STATE_DOCKED,
|
||||
STATE_ERROR,
|
||||
)
|
||||
from homeassistant.const import ATTR_COMMAND, ATTR_ENTITY_ID
|
||||
from homeassistant.util.dt import utcnow
|
||||
|
||||
from .conftest import setup_hub
|
||||
from .conftest import setup_integration
|
||||
|
||||
from tests.common import async_fire_time_changed
|
||||
|
||||
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."""
|
||||
await setup_hub(hass, mock_hub, PLATFORM_DOMAIN)
|
||||
await setup_integration(hass, mock_account, PLATFORM_DOMAIN)
|
||||
|
||||
vacuum = hass.states.get(ENTITY_ID)
|
||||
assert vacuum
|
||||
@ -33,6 +34,15 @@ async def test_vacuum(hass, mock_hub):
|
||||
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(
|
||||
"service,command,extra",
|
||||
[
|
||||
@ -52,14 +62,22 @@ async def test_vacuum(hass, mock_hub):
|
||||
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."""
|
||||
await setup_hub(hass, mock_hub, PLATFORM_DOMAIN)
|
||||
await setup_integration(hass, mock_account, PLATFORM_DOMAIN)
|
||||
|
||||
vacuum = hass.states.get(ENTITY_ID)
|
||||
assert vacuum is not None
|
||||
assert vacuum
|
||||
assert vacuum.state == STATE_DOCKED
|
||||
|
||||
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)
|
||||
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()
|
||||
|
@ -246,6 +246,10 @@ async def test_templates_with_entities(hass, calls):
|
||||
await hass.async_block_till_done()
|
||||
_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):
|
||||
"""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_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")
|
||||
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")
|
||||
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):
|
||||
|
@ -288,6 +288,18 @@ def aeotec_radiator_thermostat_state_fixture():
|
||||
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")
|
||||
def mock_client_fixture(controller_state, version_state):
|
||||
"""Mock a client."""
|
||||
@ -484,6 +496,14 @@ def in_wall_smart_fan_control_fixture(client, in_wall_smart_fan_control_state):
|
||||
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")
|
||||
def multiple_devices_fixture(
|
||||
client, climate_radio_thermostat_ct100_plus_state, lock_schlage_be469_state
|
||||
|
@ -24,9 +24,17 @@ from homeassistant.components.climate.const import (
|
||||
SERVICE_SET_HVAC_MODE,
|
||||
SERVICE_SET_PRESET_MODE,
|
||||
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.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
ATTR_SUPPORTED_FEATURES,
|
||||
ATTR_TEMPERATURE,
|
||||
)
|
||||
|
||||
from .common import (
|
||||
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_FAN_MODE] == "Auto low"
|
||||
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
|
||||
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_HVAC_MODES] == [HVAC_MODE_HEAT]
|
||||
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()
|
||||
|
||||
@ -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_HVAC_ACTION] == CURRENT_HVAC_IDLE
|
||||
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):
|
||||
@ -507,3 +530,4 @@ async def test_thermostat_srt321_hrt4_zw(hass, client, srt321_hrt4_zw, integrati
|
||||
HVAC_MODE_HEAT,
|
||||
]
|
||||
assert state.attributes[ATTR_CURRENT_TEMPERATURE] is None
|
||||
assert state.attributes[ATTR_SUPPORTED_FEATURES] == SUPPORT_PRESET_MODE
|
||||
|
@ -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):
|
||||
"""Test we handle a non ready node that exists during integration setup."""
|
||||
node = multisensor_6
|
||||
|
414
tests/fixtures/zwave_js/null_name_check_state.json
vendored
Normal file
414
tests/fixtures/zwave_js/null_name_check_state.json
vendored
Normal 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
|
||||
}
|
||||
]
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user