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