mirror of
https://github.com/home-assistant/core.git
synced 2025-07-22 04:37:06 +00:00
Merge pull request #46249 from home-assistant/rc
This commit is contained in:
commit
0ddc7d90a4
@ -48,8 +48,11 @@ async def async_migrate_entry(hass, config_entry):
|
||||
|
||||
# Flatten configuration but keep old data if user rollbacks HASS prior to 0.106
|
||||
if config_entry.version == 1:
|
||||
config_entry.data = {**config_entry.data, **config_entry.data[CONF_DEVICE]}
|
||||
config_entry.unique_id = config_entry.data[CONF_MAC]
|
||||
unique_id = config_entry.data[CONF_MAC]
|
||||
data = {**config_entry.data, **config_entry.data[CONF_DEVICE]}
|
||||
hass.config_entries.async_update_entry(
|
||||
config_entry, unique_id=unique_id, data=data
|
||||
)
|
||||
config_entry.version = 2
|
||||
|
||||
# Normalise MAC address of device which also affects entity unique IDs
|
||||
@ -66,10 +69,12 @@ async def async_migrate_entry(hass, config_entry):
|
||||
)
|
||||
}
|
||||
|
||||
await async_migrate_entries(hass, config_entry.entry_id, update_unique_id)
|
||||
if old_unique_id != new_unique_id:
|
||||
await async_migrate_entries(hass, config_entry.entry_id, update_unique_id)
|
||||
|
||||
config_entry.unique_id = new_unique_id
|
||||
config_entry.version = 3
|
||||
hass.config_entries.async_update_entry(
|
||||
config_entry, unique_id=new_unique_id
|
||||
)
|
||||
|
||||
_LOGGER.info("Migration to version %s successful", config_entry.version)
|
||||
|
||||
|
@ -6,7 +6,7 @@ from homeassistant.const import ATTR_DEVICE_ID, CONF_EVENT
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.event import Event
|
||||
|
||||
from .const import DOMAIN as DECONZ_DOMAIN
|
||||
from .const import CONF_GESTURE, DOMAIN as DECONZ_DOMAIN
|
||||
from .deconz_event import CONF_DECONZ_EVENT, DeconzEvent
|
||||
from .device_trigger import (
|
||||
CONF_BOTH_BUTTONS,
|
||||
@ -107,8 +107,12 @@ def _get_device_event_description(modelid: str, event: str) -> tuple:
|
||||
device_event_descriptions: dict = REMOTES[modelid]
|
||||
|
||||
for event_type_tuple, event_dict in device_event_descriptions.items():
|
||||
if event == event_dict[CONF_EVENT]:
|
||||
if event == event_dict.get(CONF_EVENT):
|
||||
return event_type_tuple
|
||||
if event == event_dict.get(CONF_GESTURE):
|
||||
return event_type_tuple
|
||||
|
||||
return (None, None)
|
||||
|
||||
|
||||
@callback
|
||||
@ -125,15 +129,35 @@ def async_describe_events(
|
||||
hass, event.data[ATTR_DEVICE_ID]
|
||||
)
|
||||
|
||||
if deconz_event.device.modelid not in REMOTES:
|
||||
action = None
|
||||
interface = None
|
||||
data = event.data.get(CONF_EVENT) or event.data.get(CONF_GESTURE, "")
|
||||
|
||||
if data and deconz_event.device.modelid in REMOTES:
|
||||
action, interface = _get_device_event_description(
|
||||
deconz_event.device.modelid, data
|
||||
)
|
||||
|
||||
# Unknown event
|
||||
if not data:
|
||||
return {
|
||||
"name": f"{deconz_event.device.name}",
|
||||
"message": f"fired event '{event.data[CONF_EVENT]}'.",
|
||||
"message": "fired an unknown event.",
|
||||
}
|
||||
|
||||
action, interface = _get_device_event_description(
|
||||
deconz_event.device.modelid, event.data[CONF_EVENT]
|
||||
)
|
||||
# No device event match
|
||||
if not action:
|
||||
return {
|
||||
"name": f"{deconz_event.device.name}",
|
||||
"message": f"fired event '{data}'.",
|
||||
}
|
||||
|
||||
# Gesture event
|
||||
if not interface:
|
||||
return {
|
||||
"name": f"{deconz_event.device.name}",
|
||||
"message": f"fired event '{ACTIONS[action]}'.",
|
||||
}
|
||||
|
||||
return {
|
||||
"name": f"{deconz_event.device.name}",
|
||||
|
@ -70,8 +70,9 @@ def setup(hass, config):
|
||||
|
||||
overwrite = service.data.get(ATTR_OVERWRITE)
|
||||
|
||||
# Check the path
|
||||
raise_if_invalid_path(subdir)
|
||||
if subdir:
|
||||
# Check the path
|
||||
raise_if_invalid_path(subdir)
|
||||
|
||||
final_path = None
|
||||
|
||||
|
@ -1,10 +1,15 @@
|
||||
"""The foscam component."""
|
||||
import asyncio
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from libpyfoscam import FoscamCamera
|
||||
|
||||
from .const import DOMAIN, SERVICE_PTZ, SERVICE_PTZ_PRESET
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_registry import async_migrate_entries
|
||||
|
||||
from .config_flow import DEFAULT_RTSP_PORT
|
||||
from .const import CONF_RTSP_PORT, DOMAIN, LOGGER, SERVICE_PTZ, SERVICE_PTZ_PRESET
|
||||
|
||||
PLATFORMS = ["camera"]
|
||||
|
||||
@ -22,7 +27,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
|
||||
hass.config_entries.async_forward_entry_setup(entry, component)
|
||||
)
|
||||
|
||||
hass.data[DOMAIN][entry.unique_id] = entry.data
|
||||
hass.data[DOMAIN][entry.entry_id] = entry.data
|
||||
|
||||
return True
|
||||
|
||||
@ -39,10 +44,50 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
|
||||
)
|
||||
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN].pop(entry.unique_id)
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
if not hass.data[DOMAIN]:
|
||||
hass.services.async_remove(domain=DOMAIN, service=SERVICE_PTZ)
|
||||
hass.services.async_remove(domain=DOMAIN, service=SERVICE_PTZ_PRESET)
|
||||
|
||||
return unload_ok
|
||||
|
||||
|
||||
async def async_migrate_entry(hass, config_entry: ConfigEntry):
|
||||
"""Migrate old entry."""
|
||||
LOGGER.debug("Migrating from version %s", config_entry.version)
|
||||
|
||||
if config_entry.version == 1:
|
||||
# Change unique id
|
||||
@callback
|
||||
def update_unique_id(entry):
|
||||
return {"new_unique_id": config_entry.entry_id}
|
||||
|
||||
await async_migrate_entries(hass, config_entry.entry_id, update_unique_id)
|
||||
|
||||
config_entry.unique_id = None
|
||||
|
||||
# Get RTSP port from the camera or use the fallback one and store it in data
|
||||
camera = FoscamCamera(
|
||||
config_entry.data[CONF_HOST],
|
||||
config_entry.data[CONF_PORT],
|
||||
config_entry.data[CONF_USERNAME],
|
||||
config_entry.data[CONF_PASSWORD],
|
||||
verbose=False,
|
||||
)
|
||||
|
||||
ret, response = await hass.async_add_executor_job(camera.get_port_info)
|
||||
|
||||
rtsp_port = DEFAULT_RTSP_PORT
|
||||
|
||||
if ret != 0:
|
||||
rtsp_port = response.get("rtspPort") or response.get("mediaPort")
|
||||
|
||||
config_entry.data = {**config_entry.data, CONF_RTSP_PORT: rtsp_port}
|
||||
|
||||
# Change entry version
|
||||
config_entry.version = 2
|
||||
|
||||
LOGGER.info("Migration to version %s successful", config_entry.version)
|
||||
|
||||
return True
|
||||
|
@ -15,7 +15,14 @@ from homeassistant.const import (
|
||||
)
|
||||
from homeassistant.helpers import config_validation as cv, entity_platform
|
||||
|
||||
from .const import CONF_STREAM, DOMAIN, LOGGER, SERVICE_PTZ, SERVICE_PTZ_PRESET
|
||||
from .const import (
|
||||
CONF_RTSP_PORT,
|
||||
CONF_STREAM,
|
||||
DOMAIN,
|
||||
LOGGER,
|
||||
SERVICE_PTZ,
|
||||
SERVICE_PTZ_PRESET,
|
||||
)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
@ -24,7 +31,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Optional(CONF_NAME, default="Foscam Camera"): cv.string,
|
||||
vol.Optional(CONF_PORT, default=88): cv.port,
|
||||
vol.Optional("rtsp_port"): cv.port,
|
||||
vol.Optional(CONF_RTSP_PORT): cv.port,
|
||||
}
|
||||
)
|
||||
|
||||
@ -71,6 +78,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
|
||||
CONF_USERNAME: config[CONF_USERNAME],
|
||||
CONF_PASSWORD: config[CONF_PASSWORD],
|
||||
CONF_STREAM: "Main",
|
||||
CONF_RTSP_PORT: config.get(CONF_RTSP_PORT, 554),
|
||||
}
|
||||
|
||||
hass.async_create_task(
|
||||
@ -134,8 +142,8 @@ class HassFoscamCamera(Camera):
|
||||
self._username = config_entry.data[CONF_USERNAME]
|
||||
self._password = config_entry.data[CONF_PASSWORD]
|
||||
self._stream = config_entry.data[CONF_STREAM]
|
||||
self._unique_id = config_entry.unique_id
|
||||
self._rtsp_port = None
|
||||
self._unique_id = config_entry.entry_id
|
||||
self._rtsp_port = config_entry.data[CONF_RTSP_PORT]
|
||||
self._motion_status = False
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
@ -145,7 +153,13 @@ class HassFoscamCamera(Camera):
|
||||
self._foscam_session.get_motion_detect_config
|
||||
)
|
||||
|
||||
if ret != 0:
|
||||
if ret == -3:
|
||||
LOGGER.info(
|
||||
"Can't get motion detection status, camera %s configured with non-admin user",
|
||||
self._name,
|
||||
)
|
||||
|
||||
elif ret != 0:
|
||||
LOGGER.error(
|
||||
"Error getting motion detection status of %s: %s", self._name, ret
|
||||
)
|
||||
@ -153,17 +167,6 @@ class HassFoscamCamera(Camera):
|
||||
else:
|
||||
self._motion_status = response == 1
|
||||
|
||||
# Get RTSP port
|
||||
ret, response = await self.hass.async_add_executor_job(
|
||||
self._foscam_session.get_port_info
|
||||
)
|
||||
|
||||
if ret != 0:
|
||||
LOGGER.error("Error getting RTSP port of %s: %s", self._name, ret)
|
||||
|
||||
else:
|
||||
self._rtsp_port = response.get("rtspPort") or response.get("mediaPort")
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return the entity unique ID."""
|
||||
@ -205,6 +208,11 @@ class HassFoscamCamera(Camera):
|
||||
ret = self._foscam_session.enable_motion_detection()
|
||||
|
||||
if ret != 0:
|
||||
if ret == -3:
|
||||
LOGGER.info(
|
||||
"Can't set motion detection status, camera %s configured with non-admin user",
|
||||
self._name,
|
||||
)
|
||||
return
|
||||
|
||||
self._motion_status = True
|
||||
@ -220,6 +228,11 @@ class HassFoscamCamera(Camera):
|
||||
ret = self._foscam_session.disable_motion_detection()
|
||||
|
||||
if ret != 0:
|
||||
if ret == -3:
|
||||
LOGGER.info(
|
||||
"Can't set motion detection status, camera %s configured with non-admin user",
|
||||
self._name,
|
||||
)
|
||||
return
|
||||
|
||||
self._motion_status = False
|
||||
|
@ -1,6 +1,10 @@
|
||||
"""Config flow for foscam integration."""
|
||||
from libpyfoscam import FoscamCamera
|
||||
from libpyfoscam.foscam import ERROR_FOSCAM_AUTH, ERROR_FOSCAM_UNAVAILABLE
|
||||
from libpyfoscam.foscam import (
|
||||
ERROR_FOSCAM_AUTH,
|
||||
ERROR_FOSCAM_UNAVAILABLE,
|
||||
FOSCAM_SUCCESS,
|
||||
)
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries, exceptions
|
||||
@ -13,12 +17,13 @@ from homeassistant.const import (
|
||||
)
|
||||
from homeassistant.data_entry_flow import AbortFlow
|
||||
|
||||
from .const import CONF_STREAM, LOGGER
|
||||
from .const import CONF_RTSP_PORT, CONF_STREAM, LOGGER
|
||||
from .const import DOMAIN # pylint:disable=unused-import
|
||||
|
||||
STREAMS = ["Main", "Sub"]
|
||||
|
||||
DEFAULT_PORT = 88
|
||||
DEFAULT_RTSP_PORT = 554
|
||||
|
||||
|
||||
DATA_SCHEMA = vol.Schema(
|
||||
@ -28,6 +33,7 @@ DATA_SCHEMA = vol.Schema(
|
||||
vol.Required(CONF_USERNAME): str,
|
||||
vol.Required(CONF_PASSWORD): str,
|
||||
vol.Required(CONF_STREAM, default=STREAMS[0]): vol.In(STREAMS),
|
||||
vol.Required(CONF_RTSP_PORT, default=DEFAULT_RTSP_PORT): int,
|
||||
}
|
||||
)
|
||||
|
||||
@ -35,7 +41,7 @@ DATA_SCHEMA = vol.Schema(
|
||||
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for foscam."""
|
||||
|
||||
VERSION = 1
|
||||
VERSION = 2
|
||||
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
|
||||
|
||||
async def _validate_and_create(self, data):
|
||||
@ -43,6 +49,14 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
|
||||
Data has the keys from DATA_SCHEMA with values provided by the user.
|
||||
"""
|
||||
|
||||
for entry in self.hass.config_entries.async_entries(DOMAIN):
|
||||
if (
|
||||
entry.data[CONF_HOST] == data[CONF_HOST]
|
||||
and entry.data[CONF_PORT] == data[CONF_PORT]
|
||||
):
|
||||
raise AbortFlow("already_configured")
|
||||
|
||||
camera = FoscamCamera(
|
||||
data[CONF_HOST],
|
||||
data[CONF_PORT],
|
||||
@ -52,7 +66,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
)
|
||||
|
||||
# Validate data by sending a request to the camera
|
||||
ret, response = await self.hass.async_add_executor_job(camera.get_dev_info)
|
||||
ret, _ = await self.hass.async_add_executor_job(camera.get_product_all_info)
|
||||
|
||||
if ret == ERROR_FOSCAM_UNAVAILABLE:
|
||||
raise CannotConnect
|
||||
@ -60,10 +74,23 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
if ret == ERROR_FOSCAM_AUTH:
|
||||
raise InvalidAuth
|
||||
|
||||
await self.async_set_unique_id(response["mac"])
|
||||
self._abort_if_unique_id_configured()
|
||||
if ret != FOSCAM_SUCCESS:
|
||||
LOGGER.error(
|
||||
"Unexpected error code from camera %s:%s: %s",
|
||||
data[CONF_HOST],
|
||||
data[CONF_PORT],
|
||||
ret,
|
||||
)
|
||||
raise InvalidResponse
|
||||
|
||||
name = data.pop(CONF_NAME, response["devName"])
|
||||
# Try to get camera name (only possible with admin account)
|
||||
ret, response = await self.hass.async_add_executor_job(camera.get_dev_info)
|
||||
|
||||
dev_name = response.get(
|
||||
"devName", f"Foscam {data[CONF_HOST]}:{data[CONF_PORT]}"
|
||||
)
|
||||
|
||||
name = data.pop(CONF_NAME, dev_name)
|
||||
|
||||
return self.async_create_entry(title=name, data=data)
|
||||
|
||||
@ -81,6 +108,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
except InvalidAuth:
|
||||
errors["base"] = "invalid_auth"
|
||||
|
||||
except InvalidResponse:
|
||||
errors["base"] = "invalid_response"
|
||||
|
||||
except AbortFlow:
|
||||
raise
|
||||
|
||||
@ -105,6 +135,12 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
LOGGER.error("Error importing foscam platform config: invalid auth.")
|
||||
return self.async_abort(reason="invalid_auth")
|
||||
|
||||
except InvalidResponse:
|
||||
LOGGER.exception(
|
||||
"Error importing foscam platform config: invalid response from camera."
|
||||
)
|
||||
return self.async_abort(reason="invalid_response")
|
||||
|
||||
except AbortFlow:
|
||||
raise
|
||||
|
||||
@ -121,3 +157,7 @@ class CannotConnect(exceptions.HomeAssistantError):
|
||||
|
||||
class InvalidAuth(exceptions.HomeAssistantError):
|
||||
"""Error to indicate there is invalid auth."""
|
||||
|
||||
|
||||
class InvalidResponse(exceptions.HomeAssistantError):
|
||||
"""Error to indicate there is invalid response."""
|
||||
|
@ -5,6 +5,7 @@ LOGGER = logging.getLogger(__package__)
|
||||
|
||||
DOMAIN = "foscam"
|
||||
|
||||
CONF_RTSP_PORT = "rtsp_port"
|
||||
CONF_STREAM = "stream"
|
||||
|
||||
SERVICE_PTZ = "ptz"
|
||||
|
@ -8,6 +8,7 @@
|
||||
"port": "[%key:common::config_flow::data::port%]",
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"rtsp_port": "RTSP port",
|
||||
"stream": "Stream"
|
||||
}
|
||||
}
|
||||
@ -15,6 +16,7 @@
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"invalid_response": "Invalid response from the device",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"abort": {
|
||||
|
@ -6,6 +6,7 @@
|
||||
"error": {
|
||||
"cannot_connect": "Failed to connect",
|
||||
"invalid_auth": "Invalid authentication",
|
||||
"invalid_response": "Invalid response from the device",
|
||||
"unknown": "Unexpected error"
|
||||
},
|
||||
"step": {
|
||||
@ -14,6 +15,7 @@
|
||||
"host": "Host",
|
||||
"password": "Password",
|
||||
"port": "Port",
|
||||
"rtsp_port": "RTSP port",
|
||||
"stream": "Stream",
|
||||
"username": "Username"
|
||||
}
|
||||
|
@ -2,6 +2,6 @@
|
||||
"domain": "google_translate",
|
||||
"name": "Google Translate Text-to-Speech",
|
||||
"documentation": "https://www.home-assistant.io/integrations/google_translate",
|
||||
"requirements": ["gTTS==2.2.1"],
|
||||
"requirements": ["gTTS==2.2.2"],
|
||||
"codeowners": []
|
||||
}
|
||||
|
@ -595,10 +595,15 @@ class WaterHeater(HomeAccessory):
|
||||
def async_update_state(self, new_state):
|
||||
"""Update water_heater state after state change."""
|
||||
# Update current and target temperature
|
||||
temperature = _get_target_temperature(new_state, self._unit)
|
||||
if temperature is not None:
|
||||
if temperature != self.char_current_temp.value:
|
||||
self.char_target_temp.set_value(temperature)
|
||||
target_temperature = _get_target_temperature(new_state, self._unit)
|
||||
if target_temperature is not None:
|
||||
if target_temperature != self.char_target_temp.value:
|
||||
self.char_target_temp.set_value(target_temperature)
|
||||
|
||||
current_temperature = _get_current_temperature(new_state, self._unit)
|
||||
if current_temperature is not None:
|
||||
if current_temperature != self.char_current_temp.value:
|
||||
self.char_current_temp.set_value(current_temperature)
|
||||
|
||||
# Update display units
|
||||
if self._unit and self._unit in UNIT_HASS_TO_HOMEKIT:
|
||||
|
@ -290,7 +290,7 @@ class KNXModule:
|
||||
if CONF_KNX_ROUTING in self.config[DOMAIN]:
|
||||
return self.connection_config_routing()
|
||||
# config from xknx.yaml always has priority later on
|
||||
return ConnectionConfig()
|
||||
return ConnectionConfig(auto_reconnect=True)
|
||||
|
||||
def connection_config_routing(self):
|
||||
"""Return the connection_config if routing is configured."""
|
||||
|
@ -28,7 +28,6 @@ from homeassistant.components.climate.const import (
|
||||
from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.util.temperature import convert as convert_temperature
|
||||
|
||||
from .const import DATA_UNSUBSCRIBE, DOMAIN
|
||||
from .entity import ZWaveDeviceEntity
|
||||
@ -155,13 +154,6 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
)
|
||||
|
||||
|
||||
def convert_units(units):
|
||||
"""Return units as a farenheit or celsius constant."""
|
||||
if units == "F":
|
||||
return TEMP_FAHRENHEIT
|
||||
return TEMP_CELSIUS
|
||||
|
||||
|
||||
class ZWaveClimateEntity(ZWaveDeviceEntity, ClimateEntity):
|
||||
"""Representation of a Z-Wave Climate device."""
|
||||
|
||||
@ -207,18 +199,16 @@ class ZWaveClimateEntity(ZWaveDeviceEntity, ClimateEntity):
|
||||
@property
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement."""
|
||||
return convert_units(self._current_mode_setpoint_values[0].units)
|
||||
if self.values.temperature is not None and self.values.temperature.units == "F":
|
||||
return TEMP_FAHRENHEIT
|
||||
return TEMP_CELSIUS
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
"""Return the current temperature."""
|
||||
if not self.values.temperature:
|
||||
return None
|
||||
return convert_temperature(
|
||||
self.values.temperature.value,
|
||||
convert_units(self._current_mode_setpoint_values[0].units),
|
||||
self.temperature_unit,
|
||||
)
|
||||
return self.values.temperature.value
|
||||
|
||||
@property
|
||||
def hvac_action(self):
|
||||
@ -246,29 +236,17 @@ class ZWaveClimateEntity(ZWaveDeviceEntity, ClimateEntity):
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the temperature we try to reach."""
|
||||
return convert_temperature(
|
||||
self._current_mode_setpoint_values[0].value,
|
||||
convert_units(self._current_mode_setpoint_values[0].units),
|
||||
self.temperature_unit,
|
||||
)
|
||||
return self._current_mode_setpoint_values[0].value
|
||||
|
||||
@property
|
||||
def target_temperature_low(self) -> Optional[float]:
|
||||
"""Return the lowbound target temperature we try to reach."""
|
||||
return convert_temperature(
|
||||
self._current_mode_setpoint_values[0].value,
|
||||
convert_units(self._current_mode_setpoint_values[0].units),
|
||||
self.temperature_unit,
|
||||
)
|
||||
return self._current_mode_setpoint_values[0].value
|
||||
|
||||
@property
|
||||
def target_temperature_high(self) -> Optional[float]:
|
||||
"""Return the highbound target temperature we try to reach."""
|
||||
return convert_temperature(
|
||||
self._current_mode_setpoint_values[1].value,
|
||||
convert_units(self._current_mode_setpoint_values[1].units),
|
||||
self.temperature_unit,
|
||||
)
|
||||
return self._current_mode_setpoint_values[1].value
|
||||
|
||||
async def async_set_temperature(self, **kwargs):
|
||||
"""Set new target temperature.
|
||||
@ -284,29 +262,14 @@ class ZWaveClimateEntity(ZWaveDeviceEntity, ClimateEntity):
|
||||
setpoint = self._current_mode_setpoint_values[0]
|
||||
target_temp = kwargs.get(ATTR_TEMPERATURE)
|
||||
if setpoint is not None and target_temp is not None:
|
||||
target_temp = convert_temperature(
|
||||
target_temp,
|
||||
self.temperature_unit,
|
||||
convert_units(setpoint.units),
|
||||
)
|
||||
setpoint.send_value(target_temp)
|
||||
elif len(self._current_mode_setpoint_values) == 2:
|
||||
(setpoint_low, setpoint_high) = self._current_mode_setpoint_values
|
||||
target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW)
|
||||
target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH)
|
||||
if setpoint_low is not None and target_temp_low is not None:
|
||||
target_temp_low = convert_temperature(
|
||||
target_temp_low,
|
||||
self.temperature_unit,
|
||||
convert_units(setpoint_low.units),
|
||||
)
|
||||
setpoint_low.send_value(target_temp_low)
|
||||
if setpoint_high is not None and target_temp_high is not None:
|
||||
target_temp_high = convert_temperature(
|
||||
target_temp_high,
|
||||
self.temperature_unit,
|
||||
convert_units(setpoint_high.units),
|
||||
)
|
||||
setpoint_high.send_value(target_temp_high)
|
||||
|
||||
async def async_set_fan_mode(self, fan_mode):
|
||||
|
@ -3,7 +3,12 @@ import logging
|
||||
|
||||
from sqlalchemy import ForeignKeyConstraint, MetaData, Table, text
|
||||
from sqlalchemy.engine import reflection
|
||||
from sqlalchemy.exc import InternalError, OperationalError, SQLAlchemyError
|
||||
from sqlalchemy.exc import (
|
||||
InternalError,
|
||||
OperationalError,
|
||||
ProgrammingError,
|
||||
SQLAlchemyError,
|
||||
)
|
||||
from sqlalchemy.schema import AddConstraint, DropConstraint
|
||||
|
||||
from .const import DOMAIN
|
||||
@ -69,7 +74,7 @@ def _create_index(engine, table_name, index_name):
|
||||
)
|
||||
try:
|
||||
index.create(engine)
|
||||
except OperationalError as err:
|
||||
except (InternalError, ProgrammingError, OperationalError) as err:
|
||||
lower_err_str = str(err).lower()
|
||||
|
||||
if "already exists" not in lower_err_str and "duplicate" not in lower_err_str:
|
||||
@ -78,13 +83,6 @@ def _create_index(engine, table_name, index_name):
|
||||
_LOGGER.warning(
|
||||
"Index %s already exists on %s, continuing", index_name, table_name
|
||||
)
|
||||
except InternalError as err:
|
||||
if "duplicate" not in str(err).lower():
|
||||
raise
|
||||
|
||||
_LOGGER.warning(
|
||||
"Index %s already exists on %s, continuing", index_name, table_name
|
||||
)
|
||||
|
||||
_LOGGER.debug("Finished creating %s", index_name)
|
||||
|
||||
|
@ -108,6 +108,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
|
||||
)
|
||||
return False
|
||||
|
||||
if "result" not in mylink_status:
|
||||
raise ConfigEntryNotReady("The Somfy MyLink device returned an empty result")
|
||||
|
||||
_async_migrate_entity_config(hass, entry, mylink_status)
|
||||
|
||||
undo_listener = entry.add_update_listener(_async_update_listener)
|
||||
|
@ -244,10 +244,10 @@ class TadoZoneBinarySensor(TadoZoneEntity, BinarySensorEntity):
|
||||
return
|
||||
|
||||
if self.zone_variable == "power":
|
||||
self._state = self._tado_zone_data.power
|
||||
self._state = self._tado_zone_data.power == "ON"
|
||||
|
||||
elif self.zone_variable == "link":
|
||||
self._state = self._tado_zone_data.link
|
||||
self._state = self._tado_zone_data.link == "ONLINE"
|
||||
|
||||
elif self.zone_variable == "overlay":
|
||||
self._state = self._tado_zone_data.overlay_active
|
||||
|
@ -175,7 +175,7 @@ class ZwaveDimmer(ZWaveDeviceEntity, LightEntity):
|
||||
self._refreshing = True
|
||||
self.values.primary.refresh()
|
||||
|
||||
if self._timer is not None and self._timer.isAlive():
|
||||
if self._timer is not None and self._timer.is_alive():
|
||||
self._timer.cancel()
|
||||
|
||||
self._timer = Timer(self._delay, _refresh_value)
|
||||
|
@ -293,6 +293,10 @@ class ZWaveNotificationBinarySensor(ZWaveBaseEntity, BinarySensorEntity):
|
||||
"""Initialize a ZWaveNotificationBinarySensor entity."""
|
||||
super().__init__(config_entry, client, info)
|
||||
self.state_key = state_key
|
||||
self._name = self.generate_name(
|
||||
self.info.primary_value.property_name,
|
||||
[self.info.primary_value.metadata.states[self.state_key]],
|
||||
)
|
||||
# check if we have a custom mapping for this value
|
||||
self._mapping_info = self._get_sensor_mapping()
|
||||
|
||||
@ -301,14 +305,6 @@ class ZWaveNotificationBinarySensor(ZWaveBaseEntity, BinarySensorEntity):
|
||||
"""Return if the sensor is on or off."""
|
||||
return int(self.info.primary_value.value) == int(self.state_key)
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Return default name from device name and value name combination."""
|
||||
node_name = self.info.node.name or self.info.node.device_config.description
|
||||
value_name = self.info.primary_value.property_name
|
||||
state_label = self.info.primary_value.metadata.states[self.state_key]
|
||||
return f"{node_name}: {value_name} - {state_label}"
|
||||
|
||||
@property
|
||||
def device_class(self) -> Optional[str]:
|
||||
"""Return device class."""
|
||||
|
@ -207,6 +207,9 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity):
|
||||
if self._current_mode is None:
|
||||
# Thermostat(valve) with no support for setting a mode is considered heating-only
|
||||
return HVAC_MODE_HEAT
|
||||
if self._current_mode.value is None:
|
||||
# guard missing value
|
||||
return HVAC_MODE_HEAT
|
||||
return ZW_HVAC_MODE_MAP.get(int(self._current_mode.value), HVAC_MODE_HEAT_COOL)
|
||||
|
||||
@property
|
||||
@ -219,6 +222,9 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity):
|
||||
"""Return the current running hvac operation if supported."""
|
||||
if not self._operating_state:
|
||||
return None
|
||||
if self._operating_state.value is None:
|
||||
# guard missing value
|
||||
return None
|
||||
return HVAC_CURRENT_MAP.get(int(self._operating_state.value))
|
||||
|
||||
@property
|
||||
@ -234,12 +240,18 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity):
|
||||
@property
|
||||
def target_temperature(self) -> Optional[float]:
|
||||
"""Return the temperature we try to reach."""
|
||||
if self._current_mode and self._current_mode.value is None:
|
||||
# guard missing value
|
||||
return None
|
||||
temp = self._setpoint_value(self._current_mode_setpoint_enums[0])
|
||||
return temp.value if temp else None
|
||||
|
||||
@property
|
||||
def target_temperature_high(self) -> Optional[float]:
|
||||
"""Return the highbound target temperature we try to reach."""
|
||||
if self._current_mode and self._current_mode.value is None:
|
||||
# guard missing value
|
||||
return None
|
||||
temp = self._setpoint_value(self._current_mode_setpoint_enums[1])
|
||||
return temp.value if temp else None
|
||||
|
||||
@ -251,6 +263,9 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity):
|
||||
@property
|
||||
def preset_mode(self) -> Optional[str]:
|
||||
"""Return the current preset mode, e.g., home, away, temp."""
|
||||
if self._current_mode and self._current_mode.value is None:
|
||||
# guard missing value
|
||||
return None
|
||||
if self._current_mode and int(self._current_mode.value) not in THERMOSTAT_MODES:
|
||||
return_val: str = self._current_mode.metadata.states.get(
|
||||
self._current_mode.value
|
||||
|
@ -1,6 +1,6 @@
|
||||
"""Support for Z-Wave cover devices."""
|
||||
import logging
|
||||
from typing import Any, Callable, List
|
||||
from typing import Any, Callable, List, Optional
|
||||
|
||||
from zwave_js_server.client import Client as ZwaveClient
|
||||
|
||||
@ -21,8 +21,6 @@ from .entity import ZWaveBaseEntity
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
SUPPORT_GARAGE = SUPPORT_OPEN | SUPPORT_CLOSE
|
||||
PRESS_BUTTON = True
|
||||
RELEASE_BUTTON = False
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
@ -61,13 +59,19 @@ class ZWaveCover(ZWaveBaseEntity, CoverEntity):
|
||||
"""Representation of a Z-Wave Cover device."""
|
||||
|
||||
@property
|
||||
def is_closed(self) -> bool:
|
||||
def is_closed(self) -> Optional[bool]:
|
||||
"""Return true if cover is closed."""
|
||||
if self.info.primary_value.value is None:
|
||||
# guard missing value
|
||||
return None
|
||||
return bool(self.info.primary_value.value == 0)
|
||||
|
||||
@property
|
||||
def current_cover_position(self) -> int:
|
||||
def current_cover_position(self) -> Optional[int]:
|
||||
"""Return the current position of cover where 0 means closed and 100 is fully open."""
|
||||
if self.info.primary_value.value is None:
|
||||
# guard missing value
|
||||
return None
|
||||
return round((self.info.primary_value.value / 99) * 100)
|
||||
|
||||
async def async_set_cover_position(self, **kwargs: Any) -> None:
|
||||
@ -79,17 +83,19 @@ class ZWaveCover(ZWaveBaseEntity, CoverEntity):
|
||||
|
||||
async def async_open_cover(self, **kwargs: Any) -> None:
|
||||
"""Open the cover."""
|
||||
target_value = self.get_zwave_value("Open")
|
||||
await self.info.node.async_set_value(target_value, PRESS_BUTTON)
|
||||
target_value = self.get_zwave_value("targetValue")
|
||||
await self.info.node.async_set_value(target_value, 99)
|
||||
|
||||
async def async_close_cover(self, **kwargs: Any) -> None:
|
||||
"""Close cover."""
|
||||
target_value = self.get_zwave_value("Close")
|
||||
await self.info.node.async_set_value(target_value, PRESS_BUTTON)
|
||||
target_value = self.get_zwave_value("targetValue")
|
||||
await self.info.node.async_set_value(target_value, 0)
|
||||
|
||||
async def async_stop_cover(self, **kwargs: Any) -> None:
|
||||
"""Stop cover."""
|
||||
target_value = self.get_zwave_value("Open")
|
||||
await self.info.node.async_set_value(target_value, RELEASE_BUTTON)
|
||||
target_value = self.get_zwave_value("Close")
|
||||
await self.info.node.async_set_value(target_value, RELEASE_BUTTON)
|
||||
target_value = self.get_zwave_value("Open") or self.get_zwave_value("Up")
|
||||
if target_value:
|
||||
await self.info.node.async_set_value(target_value, False)
|
||||
target_value = self.get_zwave_value("Close") or self.get_zwave_value("Down")
|
||||
if target_value:
|
||||
await self.info.node.async_set_value(target_value, False)
|
||||
|
@ -130,6 +130,7 @@ DISCOVERY_SCHEMAS = [
|
||||
"Multilevel Remote Switch",
|
||||
"Multilevel Power Switch",
|
||||
"Multilevel Scene Switch",
|
||||
"Unused",
|
||||
},
|
||||
command_class={CommandClass.SWITCH_MULTILEVEL},
|
||||
property={"currentValue"},
|
||||
@ -142,6 +143,7 @@ DISCOVERY_SCHEMAS = [
|
||||
command_class={
|
||||
CommandClass.SENSOR_BINARY,
|
||||
CommandClass.BATTERY,
|
||||
CommandClass.SENSOR_ALARM,
|
||||
},
|
||||
type={"boolean"},
|
||||
),
|
||||
|
@ -1,7 +1,7 @@
|
||||
"""Generic Z-Wave Entity Class."""
|
||||
|
||||
import logging
|
||||
from typing import Optional, Tuple, Union
|
||||
from typing import List, Optional, Tuple, Union
|
||||
|
||||
from zwave_js_server.client import Client as ZwaveClient
|
||||
from zwave_js_server.model.node import Node as ZwaveNode
|
||||
@ -35,6 +35,7 @@ class ZWaveBaseEntity(Entity):
|
||||
self.config_entry = config_entry
|
||||
self.client = client
|
||||
self.info = info
|
||||
self._name = self.generate_name()
|
||||
# entities requiring additional values, can add extra ids to this list
|
||||
self.watched_value_ids = {self.info.primary_value.value_id}
|
||||
|
||||
@ -61,19 +62,35 @@ class ZWaveBaseEntity(Entity):
|
||||
"identifiers": {get_device_id(self.client, self.info.node)},
|
||||
}
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Return default name from device name and value name combination."""
|
||||
def generate_name(
|
||||
self,
|
||||
alternate_value_name: Optional[str] = None,
|
||||
additional_info: Optional[List[str]] = None,
|
||||
) -> str:
|
||||
"""Generate entity name."""
|
||||
if additional_info is None:
|
||||
additional_info = []
|
||||
node_name = self.info.node.name or self.info.node.device_config.description
|
||||
value_name = (
|
||||
self.info.primary_value.metadata.label
|
||||
alternate_value_name
|
||||
or self.info.primary_value.metadata.label
|
||||
or self.info.primary_value.property_key_name
|
||||
or self.info.primary_value.property_name
|
||||
)
|
||||
name = f"{node_name}: {value_name}"
|
||||
for item in additional_info:
|
||||
if item:
|
||||
name += f" - {item}"
|
||||
# append endpoint if > 1
|
||||
if self.info.primary_value.endpoint > 1:
|
||||
value_name += f" ({self.info.primary_value.endpoint})"
|
||||
return f"{node_name}: {value_name}"
|
||||
name += f" ({self.info.primary_value.endpoint})"
|
||||
|
||||
return name
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Return default name from device name and value name combination."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def unique_id(self) -> str:
|
||||
@ -83,13 +100,7 @@ class ZWaveBaseEntity(Entity):
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return entity availability."""
|
||||
return (
|
||||
self.client.connected
|
||||
and bool(self.info.node.ready)
|
||||
# a None value indicates something wrong with the device,
|
||||
# or the value is simply not yet there (it will arrive later).
|
||||
and self.info.primary_value.value is not None
|
||||
)
|
||||
return self.client.connected and bool(self.info.node.ready)
|
||||
|
||||
@callback
|
||||
def _value_changed(self, event_data: dict) -> None:
|
||||
|
@ -87,8 +87,11 @@ class ZwaveFan(ZWaveBaseEntity, FanEntity):
|
||||
await self.info.node.async_set_value(target_value, 0)
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
def is_on(self) -> Optional[bool]: # type: ignore
|
||||
"""Return true if device is on (speed above 0)."""
|
||||
if self.info.primary_value.value is None:
|
||||
# guard missing value
|
||||
return None
|
||||
return bool(self.info.primary_value.value > 0)
|
||||
|
||||
@property
|
||||
@ -98,6 +101,10 @@ class ZwaveFan(ZWaveBaseEntity, FanEntity):
|
||||
The Z-Wave speed value is a byte 0-255. 255 means previous value.
|
||||
The normal range of the speed is 0-99. 0 means off.
|
||||
"""
|
||||
if self.info.primary_value.value is None:
|
||||
# guard missing value
|
||||
return None
|
||||
|
||||
value = math.ceil(self.info.primary_value.value * 3 / 100)
|
||||
return VALUE_TO_SPEED.get(value, self._previous_speed)
|
||||
|
||||
|
@ -90,6 +90,9 @@ class ZWaveLock(ZWaveBaseEntity, LockEntity):
|
||||
@property
|
||||
def is_locked(self) -> Optional[bool]:
|
||||
"""Return true if the lock is locked."""
|
||||
if self.info.primary_value.value is None:
|
||||
# guard missing value
|
||||
return None
|
||||
return int(
|
||||
LOCK_CMD_CLASS_TO_LOCKED_STATE_MAP[
|
||||
CommandClass(self.info.primary_value.command_class)
|
||||
|
@ -123,6 +123,17 @@ class ZWaveStringSensor(ZwaveSensorBase):
|
||||
class ZWaveNumericSensor(ZwaveSensorBase):
|
||||
"""Representation of a Z-Wave Numeric sensor."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config_entry: ConfigEntry,
|
||||
client: ZwaveClient,
|
||||
info: ZwaveDiscoveryInfo,
|
||||
) -> None:
|
||||
"""Initialize a ZWaveNumericSensor entity."""
|
||||
super().__init__(config_entry, client, info)
|
||||
if self.info.primary_value.command_class == CommandClass.BASIC:
|
||||
self._name = self.generate_name(self.info.primary_value.command_class_name)
|
||||
|
||||
@property
|
||||
def state(self) -> float:
|
||||
"""Return state of the sensor."""
|
||||
@ -142,19 +153,23 @@ class ZWaveNumericSensor(ZwaveSensorBase):
|
||||
|
||||
return str(self.info.primary_value.metadata.unit)
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Return default name from device name and value name combination."""
|
||||
if self.info.primary_value.command_class == CommandClass.BASIC:
|
||||
node_name = self.info.node.name or self.info.node.device_config.description
|
||||
label = self.info.primary_value.command_class_name
|
||||
return f"{node_name}: {label}"
|
||||
return super().name
|
||||
|
||||
|
||||
class ZWaveListSensor(ZwaveSensorBase):
|
||||
"""Representation of a Z-Wave Numeric sensor with multiple states."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config_entry: ConfigEntry,
|
||||
client: ZwaveClient,
|
||||
info: ZwaveDiscoveryInfo,
|
||||
) -> None:
|
||||
"""Initialize a ZWaveListSensor entity."""
|
||||
super().__init__(config_entry, client, info)
|
||||
self._name = self.generate_name(
|
||||
self.info.primary_value.property_name,
|
||||
[self.info.primary_value.property_key_name],
|
||||
)
|
||||
|
||||
@property
|
||||
def state(self) -> Optional[str]:
|
||||
"""Return state of the sensor."""
|
||||
@ -164,7 +179,7 @@ class ZWaveListSensor(ZwaveSensorBase):
|
||||
not str(self.info.primary_value.value)
|
||||
in self.info.primary_value.metadata.states
|
||||
):
|
||||
return None
|
||||
return str(self.info.primary_value.value)
|
||||
return str(
|
||||
self.info.primary_value.metadata.states[str(self.info.primary_value.value)]
|
||||
)
|
||||
@ -174,11 +189,3 @@ class ZWaveListSensor(ZwaveSensorBase):
|
||||
"""Return the device specific state attributes."""
|
||||
# add the value's int value as property for multi-value (list) items
|
||||
return {"value": self.info.primary_value.value}
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Return default name from device name and value name combination."""
|
||||
node_name = self.info.node.name or self.info.node.device_config.description
|
||||
prop_name = self.info.primary_value.property_name
|
||||
prop_key_name = self.info.primary_value.property_key_name
|
||||
return f"{node_name}: {prop_name} - {prop_key_name}"
|
||||
|
@ -1,7 +1,7 @@
|
||||
"""Representation of Z-Wave switches."""
|
||||
|
||||
import logging
|
||||
from typing import Any, Callable, List
|
||||
from typing import Any, Callable, List, Optional
|
||||
|
||||
from zwave_js_server.client import Client as ZwaveClient
|
||||
|
||||
@ -44,8 +44,11 @@ class ZWaveSwitch(ZWaveBaseEntity, SwitchEntity):
|
||||
"""Representation of a Z-Wave switch."""
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
def is_on(self) -> Optional[bool]: # type: ignore
|
||||
"""Return a boolean for the state of the switch."""
|
||||
if self.info.primary_value.value is None:
|
||||
# guard missing value
|
||||
return None
|
||||
return bool(self.info.primary_value.value)
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
|
@ -1,7 +1,7 @@
|
||||
"""Constants used by Home Assistant components."""
|
||||
MAJOR_VERSION = 2021
|
||||
MINOR_VERSION = 2
|
||||
PATCH_VERSION = "1"
|
||||
PATCH_VERSION = "2"
|
||||
__short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||
__version__ = f"{__short_version__}.{PATCH_VERSION}"
|
||||
REQUIRED_PYTHON_VER = (3, 8, 0)
|
||||
|
@ -622,7 +622,7 @@ freesms==0.1.2
|
||||
fritzconnection==1.4.0
|
||||
|
||||
# homeassistant.components.google_translate
|
||||
gTTS==2.2.1
|
||||
gTTS==2.2.2
|
||||
|
||||
# homeassistant.components.garmin_connect
|
||||
garminconnect==0.1.16
|
||||
|
@ -316,7 +316,7 @@ foobot_async==1.0.0
|
||||
fritzconnection==1.4.0
|
||||
|
||||
# homeassistant.components.google_translate
|
||||
gTTS==2.2.1
|
||||
gTTS==2.2.2
|
||||
|
||||
# homeassistant.components.garmin_connect
|
||||
garminconnect==0.1.16
|
||||
|
@ -109,7 +109,7 @@ async def test_migrate_entry(hass):
|
||||
CONF_MODEL: "model",
|
||||
CONF_NAME: "name",
|
||||
}
|
||||
assert entry.version == 3
|
||||
assert entry.version == 2 # Keep version to support rollbacking
|
||||
assert entry.unique_id == "00:40:8c:12:34:56"
|
||||
|
||||
vmd4_entity = registry.async_get("binary_sensor.vmd4")
|
||||
|
@ -3,6 +3,7 @@
|
||||
from copy import deepcopy
|
||||
|
||||
from homeassistant.components import logbook
|
||||
from homeassistant.components.deconz.const import CONF_GESTURE
|
||||
from homeassistant.components.deconz.deconz_event import CONF_DECONZ_EVENT
|
||||
from homeassistant.components.deconz.gateway import get_gateway_from_config_entry
|
||||
from homeassistant.const import CONF_DEVICE_ID, CONF_EVENT, CONF_ID, CONF_UNIQUE_ID
|
||||
@ -34,6 +35,23 @@ async def test_humanifying_deconz_event(hass):
|
||||
"config": {},
|
||||
"uniqueid": "00:00:00:00:00:00:00:02-00",
|
||||
},
|
||||
"2": {
|
||||
"id": "Xiaomi cube id",
|
||||
"name": "Xiaomi cube",
|
||||
"type": "ZHASwitch",
|
||||
"modelid": "lumi.sensor_cube",
|
||||
"state": {"buttonevent": 1000, "gesture": 1},
|
||||
"config": {},
|
||||
"uniqueid": "00:00:00:00:00:00:00:03-00",
|
||||
},
|
||||
"3": {
|
||||
"id": "faulty",
|
||||
"name": "Faulty event",
|
||||
"type": "ZHASwitch",
|
||||
"state": {},
|
||||
"config": {},
|
||||
"uniqueid": "00:00:00:00:00:00:00:04-00",
|
||||
},
|
||||
}
|
||||
config_entry = await setup_deconz_integration(hass, get_state_response=data)
|
||||
gateway = get_gateway_from_config_entry(hass, config_entry)
|
||||
@ -46,6 +64,7 @@ async def test_humanifying_deconz_event(hass):
|
||||
logbook.humanify(
|
||||
hass,
|
||||
[
|
||||
# Event without matching device trigger
|
||||
MockLazyEventPartialState(
|
||||
CONF_DECONZ_EVENT,
|
||||
{
|
||||
@ -55,6 +74,7 @@ async def test_humanifying_deconz_event(hass):
|
||||
CONF_UNIQUE_ID: gateway.events[0].serial,
|
||||
},
|
||||
),
|
||||
# Event with matching device trigger
|
||||
MockLazyEventPartialState(
|
||||
CONF_DECONZ_EVENT,
|
||||
{
|
||||
@ -64,6 +84,36 @@ async def test_humanifying_deconz_event(hass):
|
||||
CONF_UNIQUE_ID: gateway.events[1].serial,
|
||||
},
|
||||
),
|
||||
# Gesture with matching device trigger
|
||||
MockLazyEventPartialState(
|
||||
CONF_DECONZ_EVENT,
|
||||
{
|
||||
CONF_DEVICE_ID: gateway.events[2].device_id,
|
||||
CONF_GESTURE: 1,
|
||||
CONF_ID: gateway.events[2].event_id,
|
||||
CONF_UNIQUE_ID: gateway.events[2].serial,
|
||||
},
|
||||
),
|
||||
# Unsupported device trigger
|
||||
MockLazyEventPartialState(
|
||||
CONF_DECONZ_EVENT,
|
||||
{
|
||||
CONF_DEVICE_ID: gateway.events[2].device_id,
|
||||
CONF_GESTURE: "unsupported_gesture",
|
||||
CONF_ID: gateway.events[2].event_id,
|
||||
CONF_UNIQUE_ID: gateway.events[2].serial,
|
||||
},
|
||||
),
|
||||
# Unknown event
|
||||
MockLazyEventPartialState(
|
||||
CONF_DECONZ_EVENT,
|
||||
{
|
||||
CONF_DEVICE_ID: gateway.events[3].device_id,
|
||||
"unknown_event": None,
|
||||
CONF_ID: gateway.events[3].event_id,
|
||||
CONF_UNIQUE_ID: gateway.events[3].serial,
|
||||
},
|
||||
),
|
||||
],
|
||||
entity_attr_cache,
|
||||
{},
|
||||
@ -77,3 +127,15 @@ async def test_humanifying_deconz_event(hass):
|
||||
assert events[1]["name"] == "Hue remote"
|
||||
assert events[1]["domain"] == "deconz"
|
||||
assert events[1]["message"] == "'Long press' event for 'Dim up' was fired."
|
||||
|
||||
assert events[2]["name"] == "Xiaomi cube"
|
||||
assert events[2]["domain"] == "deconz"
|
||||
assert events[2]["message"] == "fired event 'Shake'."
|
||||
|
||||
assert events[3]["name"] == "Xiaomi cube"
|
||||
assert events[3]["domain"] == "deconz"
|
||||
assert events[3]["message"] == "fired event 'unsupported_gesture'."
|
||||
|
||||
assert events[4]["name"] == "Faulty event"
|
||||
assert events[4]["domain"] == "deconz"
|
||||
assert events[4]["message"] == "fired an unknown event."
|
||||
|
@ -1,7 +1,12 @@
|
||||
"""Test the Foscam config flow."""
|
||||
from unittest.mock import patch
|
||||
|
||||
from libpyfoscam.foscam import ERROR_FOSCAM_AUTH, ERROR_FOSCAM_UNAVAILABLE
|
||||
from libpyfoscam.foscam import (
|
||||
ERROR_FOSCAM_AUTH,
|
||||
ERROR_FOSCAM_CMD,
|
||||
ERROR_FOSCAM_UNAVAILABLE,
|
||||
ERROR_FOSCAM_UNKNOWN,
|
||||
)
|
||||
|
||||
from homeassistant import config_entries, data_entry_flow, setup
|
||||
from homeassistant.components.foscam import config_flow
|
||||
@ -14,6 +19,13 @@ VALID_CONFIG = {
|
||||
config_flow.CONF_USERNAME: "admin",
|
||||
config_flow.CONF_PASSWORD: "1234",
|
||||
config_flow.CONF_STREAM: "Main",
|
||||
config_flow.CONF_RTSP_PORT: 554,
|
||||
}
|
||||
OPERATOR_CONFIG = {
|
||||
config_flow.CONF_USERNAME: "operator",
|
||||
}
|
||||
INVALID_RESPONSE_CONFIG = {
|
||||
config_flow.CONF_USERNAME: "interr",
|
||||
}
|
||||
CAMERA_NAME = "Mocked Foscam Camera"
|
||||
CAMERA_MAC = "C0:C1:D0:F4:B4:D4"
|
||||
@ -23,26 +35,39 @@ def setup_mock_foscam_camera(mock_foscam_camera):
|
||||
"""Mock FoscamCamera simulating behaviour using a base valid config."""
|
||||
|
||||
def configure_mock_on_init(host, port, user, passwd, verbose=False):
|
||||
return_code = 0
|
||||
data = {}
|
||||
product_all_info_rc = 0
|
||||
dev_info_rc = 0
|
||||
dev_info_data = {}
|
||||
|
||||
if (
|
||||
host != VALID_CONFIG[config_flow.CONF_HOST]
|
||||
or port != VALID_CONFIG[config_flow.CONF_PORT]
|
||||
):
|
||||
return_code = ERROR_FOSCAM_UNAVAILABLE
|
||||
product_all_info_rc = dev_info_rc = ERROR_FOSCAM_UNAVAILABLE
|
||||
|
||||
elif (
|
||||
user != VALID_CONFIG[config_flow.CONF_USERNAME]
|
||||
user
|
||||
not in [
|
||||
VALID_CONFIG[config_flow.CONF_USERNAME],
|
||||
OPERATOR_CONFIG[config_flow.CONF_USERNAME],
|
||||
INVALID_RESPONSE_CONFIG[config_flow.CONF_USERNAME],
|
||||
]
|
||||
or passwd != VALID_CONFIG[config_flow.CONF_PASSWORD]
|
||||
):
|
||||
return_code = ERROR_FOSCAM_AUTH
|
||||
product_all_info_rc = dev_info_rc = ERROR_FOSCAM_AUTH
|
||||
|
||||
elif user == INVALID_RESPONSE_CONFIG[config_flow.CONF_USERNAME]:
|
||||
product_all_info_rc = dev_info_rc = ERROR_FOSCAM_UNKNOWN
|
||||
|
||||
elif user == OPERATOR_CONFIG[config_flow.CONF_USERNAME]:
|
||||
dev_info_rc = ERROR_FOSCAM_CMD
|
||||
|
||||
else:
|
||||
data["devName"] = CAMERA_NAME
|
||||
data["mac"] = CAMERA_MAC
|
||||
dev_info_data["devName"] = CAMERA_NAME
|
||||
dev_info_data["mac"] = CAMERA_MAC
|
||||
|
||||
mock_foscam_camera.get_dev_info.return_value = (return_code, data)
|
||||
mock_foscam_camera.get_product_all_info.return_value = (product_all_info_rc, {})
|
||||
mock_foscam_camera.get_dev_info.return_value = (dev_info_rc, dev_info_data)
|
||||
|
||||
return mock_foscam_camera
|
||||
|
||||
@ -142,12 +167,44 @@ async def test_user_cannot_connect(hass):
|
||||
assert result["errors"] == {"base": "cannot_connect"}
|
||||
|
||||
|
||||
async def test_user_invalid_response(hass):
|
||||
"""Test we handle invalid response error from user input."""
|
||||
await setup.async_setup_component(hass, "persistent_notification", {})
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["errors"] == {}
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.foscam.config_flow.FoscamCamera",
|
||||
) as mock_foscam_camera:
|
||||
setup_mock_foscam_camera(mock_foscam_camera)
|
||||
|
||||
invalid_response = VALID_CONFIG.copy()
|
||||
invalid_response[config_flow.CONF_USERNAME] = INVALID_RESPONSE_CONFIG[
|
||||
config_flow.CONF_USERNAME
|
||||
]
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
invalid_response,
|
||||
)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["errors"] == {"base": "invalid_response"}
|
||||
|
||||
|
||||
async def test_user_already_configured(hass):
|
||||
"""Test we handle already configured from user input."""
|
||||
await setup.async_setup_component(hass, "persistent_notification", {})
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=config_flow.DOMAIN, data=VALID_CONFIG, unique_id=CAMERA_MAC
|
||||
domain=config_flow.DOMAIN,
|
||||
data=VALID_CONFIG,
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
@ -201,6 +258,8 @@ async def test_user_unknown_exception(hass):
|
||||
|
||||
async def test_import_user_valid(hass):
|
||||
"""Test valid config from import."""
|
||||
await setup.async_setup_component(hass, "persistent_notification", {})
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.foscam.config_flow.FoscamCamera",
|
||||
) as mock_foscam_camera, patch(
|
||||
@ -229,6 +288,8 @@ async def test_import_user_valid(hass):
|
||||
|
||||
async def test_import_user_valid_with_name(hass):
|
||||
"""Test valid config with extra name from import."""
|
||||
await setup.async_setup_component(hass, "persistent_notification", {})
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.foscam.config_flow.FoscamCamera",
|
||||
) as mock_foscam_camera, patch(
|
||||
@ -261,10 +322,7 @@ async def test_import_user_valid_with_name(hass):
|
||||
|
||||
async def test_import_invalid_auth(hass):
|
||||
"""Test we handle invalid auth from import."""
|
||||
entry = MockConfigEntry(
|
||||
domain=config_flow.DOMAIN, data=VALID_CONFIG, unique_id=CAMERA_MAC
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
await setup.async_setup_component(hass, "persistent_notification", {})
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.foscam.config_flow.FoscamCamera",
|
||||
@ -287,11 +345,8 @@ async def test_import_invalid_auth(hass):
|
||||
|
||||
|
||||
async def test_import_cannot_connect(hass):
|
||||
"""Test we handle invalid auth from import."""
|
||||
entry = MockConfigEntry(
|
||||
domain=config_flow.DOMAIN, data=VALID_CONFIG, unique_id=CAMERA_MAC
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
"""Test we handle cannot connect error from import."""
|
||||
await setup.async_setup_component(hass, "persistent_notification", {})
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.foscam.config_flow.FoscamCamera",
|
||||
@ -313,10 +368,39 @@ async def test_import_cannot_connect(hass):
|
||||
assert result["reason"] == "cannot_connect"
|
||||
|
||||
|
||||
async def test_import_invalid_response(hass):
|
||||
"""Test we handle invalid response error from import."""
|
||||
await setup.async_setup_component(hass, "persistent_notification", {})
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.foscam.config_flow.FoscamCamera",
|
||||
) as mock_foscam_camera:
|
||||
setup_mock_foscam_camera(mock_foscam_camera)
|
||||
|
||||
invalid_response = VALID_CONFIG.copy()
|
||||
invalid_response[config_flow.CONF_USERNAME] = INVALID_RESPONSE_CONFIG[
|
||||
config_flow.CONF_USERNAME
|
||||
]
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
config_flow.DOMAIN,
|
||||
context={"source": config_entries.SOURCE_IMPORT},
|
||||
data=invalid_response,
|
||||
)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
|
||||
assert result["reason"] == "invalid_response"
|
||||
|
||||
|
||||
async def test_import_already_configured(hass):
|
||||
"""Test we handle already configured from import."""
|
||||
await setup.async_setup_component(hass, "persistent_notification", {})
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=config_flow.DOMAIN, data=VALID_CONFIG, unique_id=CAMERA_MAC
|
||||
domain=config_flow.DOMAIN,
|
||||
data=VALID_CONFIG,
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
|
@ -1599,11 +1599,15 @@ async def test_water_heater(hass, hk_driver, events):
|
||||
hass.states.async_set(
|
||||
entity_id,
|
||||
HVAC_MODE_HEAT,
|
||||
{ATTR_HVAC_MODE: HVAC_MODE_HEAT, ATTR_TEMPERATURE: 56.0},
|
||||
{
|
||||
ATTR_HVAC_MODE: HVAC_MODE_HEAT,
|
||||
ATTR_TEMPERATURE: 56.0,
|
||||
ATTR_CURRENT_TEMPERATURE: 35.0,
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert acc.char_target_temp.value == 56.0
|
||||
assert acc.char_current_temp.value == 50.0
|
||||
assert acc.char_current_temp.value == 35.0
|
||||
assert acc.char_target_heat_cool.value == 1
|
||||
assert acc.char_current_heat_cool.value == 1
|
||||
assert acc.char_display_units.value == 0
|
||||
|
@ -16,8 +16,6 @@ from homeassistant.components.climate.const import (
|
||||
HVAC_MODE_HEAT_COOL,
|
||||
HVAC_MODE_OFF,
|
||||
)
|
||||
from homeassistant.components.ozw.climate import convert_units
|
||||
from homeassistant.const import TEMP_FAHRENHEIT
|
||||
|
||||
from .common import setup_ozw
|
||||
|
||||
@ -38,8 +36,8 @@ async def test_climate(hass, climate_data, sent_messages, climate_msg, caplog):
|
||||
HVAC_MODE_HEAT_COOL,
|
||||
]
|
||||
assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE
|
||||
assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 73.5
|
||||
assert state.attributes[ATTR_TEMPERATURE] == 70.0
|
||||
assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 23.1
|
||||
assert state.attributes[ATTR_TEMPERATURE] == 21.1
|
||||
assert state.attributes.get(ATTR_TARGET_TEMP_LOW) is None
|
||||
assert state.attributes.get(ATTR_TARGET_TEMP_HIGH) is None
|
||||
assert state.attributes[ATTR_FAN_MODE] == "Auto Low"
|
||||
@ -56,7 +54,7 @@ async def test_climate(hass, climate_data, sent_messages, climate_msg, caplog):
|
||||
msg = sent_messages[-1]
|
||||
assert msg["topic"] == "OpenZWave/1/command/setvalue/"
|
||||
# Celsius is converted to Fahrenheit here!
|
||||
assert round(msg["payload"]["Value"], 2) == 26.1
|
||||
assert round(msg["payload"]["Value"], 2) == 78.98
|
||||
assert msg["payload"]["ValueIDKey"] == 281475099443218
|
||||
|
||||
# Test hvac_mode with set_temperature
|
||||
@ -74,7 +72,7 @@ async def test_climate(hass, climate_data, sent_messages, climate_msg, caplog):
|
||||
msg = sent_messages[-1]
|
||||
assert msg["topic"] == "OpenZWave/1/command/setvalue/"
|
||||
# Celsius is converted to Fahrenheit here!
|
||||
assert round(msg["payload"]["Value"], 2) == 24.1
|
||||
assert round(msg["payload"]["Value"], 2) == 75.38
|
||||
assert msg["payload"]["ValueIDKey"] == 281475099443218
|
||||
|
||||
# Test set mode
|
||||
@ -129,8 +127,8 @@ async def test_climate(hass, climate_data, sent_messages, climate_msg, caplog):
|
||||
assert state is not None
|
||||
assert state.state == HVAC_MODE_HEAT_COOL
|
||||
assert state.attributes.get(ATTR_TEMPERATURE) is None
|
||||
assert state.attributes[ATTR_TARGET_TEMP_LOW] == 70.0
|
||||
assert state.attributes[ATTR_TARGET_TEMP_HIGH] == 78.0
|
||||
assert state.attributes[ATTR_TARGET_TEMP_LOW] == 21.1
|
||||
assert state.attributes[ATTR_TARGET_TEMP_HIGH] == 25.6
|
||||
|
||||
# Test setting high/low temp on multiple setpoints
|
||||
await hass.services.async_call(
|
||||
@ -146,11 +144,11 @@ async def test_climate(hass, climate_data, sent_messages, climate_msg, caplog):
|
||||
assert len(sent_messages) == 7 # 2 messages !
|
||||
msg = sent_messages[-2] # low setpoint
|
||||
assert msg["topic"] == "OpenZWave/1/command/setvalue/"
|
||||
assert round(msg["payload"]["Value"], 2) == 20.0
|
||||
assert round(msg["payload"]["Value"], 2) == 68.0
|
||||
assert msg["payload"]["ValueIDKey"] == 281475099443218
|
||||
msg = sent_messages[-1] # high setpoint
|
||||
assert msg["topic"] == "OpenZWave/1/command/setvalue/"
|
||||
assert round(msg["payload"]["Value"], 2) == 25.0
|
||||
assert round(msg["payload"]["Value"], 2) == 77.0
|
||||
assert msg["payload"]["ValueIDKey"] == 562950076153874
|
||||
|
||||
# Test basic/single-setpoint thermostat (node 16 in dump)
|
||||
@ -327,5 +325,3 @@ async def test_climate(hass, climate_data, sent_messages, climate_msg, caplog):
|
||||
)
|
||||
assert len(sent_messages) == 12
|
||||
assert "does not support setting a mode" in caplog.text
|
||||
|
||||
assert convert_units("F") == TEMP_FAHRENHEIT
|
||||
|
@ -1,9 +1,10 @@
|
||||
"""The tests for the Recorder component."""
|
||||
# pylint: disable=protected-access
|
||||
from unittest.mock import call, patch
|
||||
from unittest.mock import Mock, PropertyMock, call, patch
|
||||
|
||||
import pytest
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.exc import InternalError, OperationalError, ProgrammingError
|
||||
from sqlalchemy.pool import StaticPool
|
||||
|
||||
from homeassistant.bootstrap import async_setup_component
|
||||
@ -79,3 +80,30 @@ def test_forgiving_add_index():
|
||||
engine = create_engine("sqlite://", poolclass=StaticPool)
|
||||
models.Base.metadata.create_all(engine)
|
||||
migration._create_index(engine, "states", "ix_states_context_id")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"exception_type", [OperationalError, ProgrammingError, InternalError]
|
||||
)
|
||||
def test_forgiving_add_index_with_other_db_types(caplog, exception_type):
|
||||
"""Test that add index will continue if index exists on mysql and postgres."""
|
||||
mocked_index = Mock()
|
||||
type(mocked_index).name = "ix_states_context_id"
|
||||
mocked_index.create = Mock(
|
||||
side_effect=exception_type(
|
||||
"CREATE INDEX ix_states_old_state_id ON states (old_state_id);",
|
||||
[],
|
||||
'relation "ix_states_old_state_id" already exists',
|
||||
)
|
||||
)
|
||||
|
||||
mocked_table = Mock()
|
||||
type(mocked_table).indexes = PropertyMock(return_value=[mocked_index])
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.recorder.migration.Table", return_value=mocked_table
|
||||
):
|
||||
migration._create_index(Mock(), "states", "ix_states_context_id")
|
||||
|
||||
assert "already exists on states" in caplog.text
|
||||
assert "continuing" in caplog.text
|
||||
|
@ -95,14 +95,16 @@ async def test_cover(hass, client, chain_actuator_zws12, integration):
|
||||
"commandClassName": "Multilevel Switch",
|
||||
"commandClass": 38,
|
||||
"endpoint": 0,
|
||||
"property": "Open",
|
||||
"propertyName": "Open",
|
||||
"property": "targetValue",
|
||||
"propertyName": "targetValue",
|
||||
"metadata": {
|
||||
"type": "boolean",
|
||||
"label": "Target value",
|
||||
"max": 99,
|
||||
"min": 0,
|
||||
"type": "number",
|
||||
"readable": True,
|
||||
"writeable": True,
|
||||
"label": "Perform a level change (Open)",
|
||||
"ccSpecific": {"switchType": 3},
|
||||
"label": "Target value",
|
||||
},
|
||||
}
|
||||
assert args["value"]
|
||||
@ -194,17 +196,19 @@ async def test_cover(hass, client, chain_actuator_zws12, integration):
|
||||
"commandClassName": "Multilevel Switch",
|
||||
"commandClass": 38,
|
||||
"endpoint": 0,
|
||||
"property": "Close",
|
||||
"propertyName": "Close",
|
||||
"property": "targetValue",
|
||||
"propertyName": "targetValue",
|
||||
"metadata": {
|
||||
"type": "boolean",
|
||||
"label": "Target value",
|
||||
"max": 99,
|
||||
"min": 0,
|
||||
"type": "number",
|
||||
"readable": True,
|
||||
"writeable": True,
|
||||
"label": "Perform a level change (Close)",
|
||||
"ccSpecific": {"switchType": 3},
|
||||
"label": "Target value",
|
||||
},
|
||||
}
|
||||
assert args["value"]
|
||||
assert args["value"] == 0
|
||||
|
||||
client.async_send_command.reset_mock()
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user