mirror of
https://github.com/home-assistant/core.git
synced 2025-07-28 15:47:12 +00:00
Add config support to zoneminder integration (#37060)
* Add config support to zoneminder integration. * Fixing spelling issue. Adding self to maintainers. Updating config flows generated file. * Maintain zoneminder functionality without breaking changes. * Addressing lint feedback. Updating code owners. * Using non-blocking calls. * Adding tests package file. * Update service description. Co-authored-by: Rohan Kapoor <rohan@rohankapoor.com> * Resolving conflicts in requirements file. * Resolving more conflicts. * Addressing PR feedback. * Merging from dev. Co-authored-by: Rohan Kapoor <rohan@rohankapoor.com>
This commit is contained in:
parent
271ffac4a9
commit
70173488a8
@ -1028,7 +1028,6 @@ omit =
|
|||||||
homeassistant/components/zhong_hong/climate.py
|
homeassistant/components/zhong_hong/climate.py
|
||||||
homeassistant/components/xbee/*
|
homeassistant/components/xbee/*
|
||||||
homeassistant/components/ziggo_mediabox_xl/media_player.py
|
homeassistant/components/ziggo_mediabox_xl/media_player.py
|
||||||
homeassistant/components/zoneminder/*
|
|
||||||
homeassistant/components/supla/*
|
homeassistant/components/supla/*
|
||||||
homeassistant/components/zwave/util.py
|
homeassistant/components/zwave/util.py
|
||||||
homeassistant/components/ozw/__init__.py
|
homeassistant/components/ozw/__init__.py
|
||||||
|
@ -504,7 +504,7 @@ homeassistant/components/zeroconf/* @Kane610
|
|||||||
homeassistant/components/zerproc/* @emlove
|
homeassistant/components/zerproc/* @emlove
|
||||||
homeassistant/components/zha/* @dmulcahey @adminiuga
|
homeassistant/components/zha/* @dmulcahey @adminiuga
|
||||||
homeassistant/components/zone/* @home-assistant/core
|
homeassistant/components/zone/* @home-assistant/core
|
||||||
homeassistant/components/zoneminder/* @rohankapoorcom
|
homeassistant/components/zoneminder/* @rohankapoorcom @vangorra
|
||||||
homeassistant/components/zwave/* @home-assistant/z-wave
|
homeassistant/components/zwave/* @home-assistant/z-wave
|
||||||
|
|
||||||
# Individual files
|
# Individual files
|
||||||
|
@ -2,97 +2,169 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
from zoneminder.zm import ZoneMinder
|
|
||||||
|
|
||||||
|
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
|
||||||
|
from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN
|
||||||
|
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
|
||||||
|
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
|
||||||
|
import homeassistant.config_entries as config_entries
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_ID,
|
ATTR_ID,
|
||||||
ATTR_NAME,
|
ATTR_NAME,
|
||||||
CONF_HOST,
|
CONF_HOST,
|
||||||
CONF_PASSWORD,
|
CONF_PASSWORD,
|
||||||
CONF_PATH,
|
CONF_PATH,
|
||||||
|
CONF_PLATFORM,
|
||||||
|
CONF_SOURCE,
|
||||||
CONF_SSL,
|
CONF_SSL,
|
||||||
CONF_USERNAME,
|
CONF_USERNAME,
|
||||||
CONF_VERIFY_SSL,
|
CONF_VERIFY_SSL,
|
||||||
)
|
)
|
||||||
import homeassistant.helpers.config_validation as cv
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.helpers.discovery import async_load_platform
|
from homeassistant.exceptions import ConfigEntryNotReady
|
||||||
|
from homeassistant.helpers import config_validation as cv
|
||||||
|
|
||||||
|
from . import const
|
||||||
|
from .common import (
|
||||||
|
ClientAvailabilityResult,
|
||||||
|
async_test_client_availability,
|
||||||
|
create_client_from_config,
|
||||||
|
del_client_from_data,
|
||||||
|
get_client_from_data,
|
||||||
|
is_client_in_data,
|
||||||
|
set_client_to_data,
|
||||||
|
set_platform_configs,
|
||||||
|
)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
PLATFORM_DOMAINS = tuple(
|
||||||
CONF_PATH_ZMS = "path_zms"
|
[BINARY_SENSOR_DOMAIN, CAMERA_DOMAIN, SENSOR_DOMAIN, SWITCH_DOMAIN]
|
||||||
|
)
|
||||||
DEFAULT_PATH = "/zm/"
|
|
||||||
DEFAULT_PATH_ZMS = "/zm/cgi-bin/nph-zms"
|
|
||||||
DEFAULT_SSL = False
|
|
||||||
DEFAULT_TIMEOUT = 10
|
|
||||||
DEFAULT_VERIFY_SSL = True
|
|
||||||
DOMAIN = "zoneminder"
|
|
||||||
|
|
||||||
HOST_CONFIG_SCHEMA = vol.Schema(
|
HOST_CONFIG_SCHEMA = vol.Schema(
|
||||||
{
|
{
|
||||||
vol.Required(CONF_HOST): cv.string,
|
vol.Required(CONF_HOST): cv.string,
|
||||||
vol.Optional(CONF_PASSWORD): cv.string,
|
vol.Optional(CONF_PASSWORD): cv.string,
|
||||||
vol.Optional(CONF_PATH, default=DEFAULT_PATH): cv.string,
|
vol.Optional(CONF_PATH, default=const.DEFAULT_PATH): cv.string,
|
||||||
vol.Optional(CONF_PATH_ZMS, default=DEFAULT_PATH_ZMS): cv.string,
|
vol.Optional(const.CONF_PATH_ZMS, default=const.DEFAULT_PATH_ZMS): cv.string,
|
||||||
vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean,
|
vol.Optional(CONF_SSL, default=const.DEFAULT_SSL): cv.boolean,
|
||||||
vol.Optional(CONF_USERNAME): cv.string,
|
vol.Optional(CONF_USERNAME): cv.string,
|
||||||
vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean,
|
vol.Optional(CONF_VERIFY_SSL, default=const.DEFAULT_VERIFY_SSL): cv.boolean,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
CONFIG_SCHEMA = vol.Schema(
|
CONFIG_SCHEMA = vol.All(
|
||||||
{DOMAIN: vol.All(cv.ensure_list, [HOST_CONFIG_SCHEMA])}, extra=vol.ALLOW_EXTRA
|
cv.deprecated(const.DOMAIN, invalidation_version="0.118"),
|
||||||
|
vol.Schema(
|
||||||
|
{const.DOMAIN: vol.All(cv.ensure_list, [HOST_CONFIG_SCHEMA])},
|
||||||
|
extra=vol.ALLOW_EXTRA,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
SERVICE_SET_RUN_STATE = "set_run_state"
|
|
||||||
SET_RUN_STATE_SCHEMA = vol.Schema(
|
SET_RUN_STATE_SCHEMA = vol.Schema(
|
||||||
{vol.Required(ATTR_ID): cv.string, vol.Required(ATTR_NAME): cv.string}
|
{vol.Required(ATTR_ID): cv.string, vol.Required(ATTR_NAME): cv.string}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def setup(hass, config):
|
async def async_setup(hass: HomeAssistant, base_config: dict):
|
||||||
"""Set up the ZoneMinder component."""
|
"""Set up the ZoneMinder component."""
|
||||||
|
|
||||||
hass.data[DOMAIN] = {}
|
# Collect the platform specific configs. It's necessary to collect these configs
|
||||||
|
# here instead of the platform's setup_platform function because the invocation order
|
||||||
success = True
|
# of setup_platform and async_setup_entry is not consistent.
|
||||||
|
set_platform_configs(
|
||||||
for conf in config[DOMAIN]:
|
hass,
|
||||||
protocol = "https" if conf[CONF_SSL] else "http"
|
SENSOR_DOMAIN,
|
||||||
|
[
|
||||||
host_name = conf[CONF_HOST]
|
platform_config
|
||||||
server_origin = f"{protocol}://{host_name}"
|
for platform_config in base_config.get(SENSOR_DOMAIN, [])
|
||||||
zm_client = ZoneMinder(
|
if platform_config[CONF_PLATFORM] == const.DOMAIN
|
||||||
server_origin,
|
],
|
||||||
conf.get(CONF_USERNAME),
|
)
|
||||||
conf.get(CONF_PASSWORD),
|
set_platform_configs(
|
||||||
conf.get(CONF_PATH),
|
hass,
|
||||||
conf.get(CONF_PATH_ZMS),
|
SWITCH_DOMAIN,
|
||||||
conf.get(CONF_VERIFY_SSL),
|
[
|
||||||
|
platform_config
|
||||||
|
for platform_config in base_config.get(SWITCH_DOMAIN, [])
|
||||||
|
if platform_config[CONF_PLATFORM] == const.DOMAIN
|
||||||
|
],
|
||||||
)
|
)
|
||||||
hass.data[DOMAIN][host_name] = zm_client
|
|
||||||
|
|
||||||
success = zm_client.login() and success
|
config = base_config.get(const.DOMAIN)
|
||||||
|
|
||||||
|
if not config:
|
||||||
|
return True
|
||||||
|
|
||||||
|
for config_item in config:
|
||||||
|
hass.async_create_task(
|
||||||
|
hass.config_entries.flow.async_init(
|
||||||
|
const.DOMAIN,
|
||||||
|
context={CONF_SOURCE: config_entries.SOURCE_IMPORT},
|
||||||
|
data=config_item,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
||||||
|
"""Set up Zoneminder config entry."""
|
||||||
|
zm_client = create_client_from_config(config_entry.data)
|
||||||
|
|
||||||
|
result = await async_test_client_availability(hass, zm_client)
|
||||||
|
if result != ClientAvailabilityResult.AVAILABLE:
|
||||||
|
raise ConfigEntryNotReady
|
||||||
|
|
||||||
|
set_client_to_data(hass, config_entry.unique_id, zm_client)
|
||||||
|
|
||||||
|
for platform_domain in PLATFORM_DOMAINS:
|
||||||
|
hass.async_create_task(
|
||||||
|
hass.config_entries.async_forward_entry_setup(config_entry, platform_domain)
|
||||||
|
)
|
||||||
|
|
||||||
|
if not hass.services.has_service(const.DOMAIN, const.SERVICE_SET_RUN_STATE):
|
||||||
|
|
||||||
|
@callback
|
||||||
def set_active_state(call):
|
def set_active_state(call):
|
||||||
"""Set the ZoneMinder run state to the given state name."""
|
"""Set the ZoneMinder run state to the given state name."""
|
||||||
zm_id = call.data[ATTR_ID]
|
zm_id = call.data[ATTR_ID]
|
||||||
state_name = call.data[ATTR_NAME]
|
state_name = call.data[ATTR_NAME]
|
||||||
if zm_id not in hass.data[DOMAIN]:
|
if not is_client_in_data(hass, zm_id):
|
||||||
_LOGGER.error("Invalid ZoneMinder host provided: %s", zm_id)
|
_LOGGER.error("Invalid ZoneMinder host provided: %s", zm_id)
|
||||||
if not hass.data[DOMAIN][zm_id].set_active_state(state_name):
|
return
|
||||||
|
|
||||||
|
if not get_client_from_data(hass, zm_id).set_active_state(state_name):
|
||||||
_LOGGER.error(
|
_LOGGER.error(
|
||||||
"Unable to change ZoneMinder state. Host: %s, state: %s",
|
"Unable to change ZoneMinder state. Host: %s, state: %s",
|
||||||
zm_id,
|
zm_id,
|
||||||
state_name,
|
state_name,
|
||||||
)
|
)
|
||||||
|
|
||||||
hass.services.register(
|
hass.services.async_register(
|
||||||
DOMAIN, SERVICE_SET_RUN_STATE, set_active_state, schema=SET_RUN_STATE_SCHEMA
|
const.DOMAIN,
|
||||||
|
const.SERVICE_SET_RUN_STATE,
|
||||||
|
set_active_state,
|
||||||
|
schema=SET_RUN_STATE_SCHEMA,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
||||||
|
"""Unload Zoneminder config entry."""
|
||||||
|
for platform_domain in PLATFORM_DOMAINS:
|
||||||
hass.async_create_task(
|
hass.async_create_task(
|
||||||
async_load_platform(hass, "binary_sensor", DOMAIN, {}, config)
|
hass.config_entries.async_forward_entry_unload(
|
||||||
|
config_entry, platform_domain
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
return success
|
# If this is the last config to exist, remove the service too.
|
||||||
|
if len(hass.config_entries.async_entries(const.DOMAIN)) <= 1:
|
||||||
|
hass.services.async_remove(const.DOMAIN, const.SERVICE_SET_RUN_STATE)
|
||||||
|
|
||||||
|
del_client_from_data(hass, config_entry.unique_id)
|
||||||
|
|
||||||
|
return True
|
||||||
|
@ -1,29 +1,43 @@
|
|||||||
"""Support for ZoneMinder binary sensors."""
|
"""Support for ZoneMinder binary sensors."""
|
||||||
|
from typing import Callable, List, Optional
|
||||||
|
|
||||||
|
from zoneminder.zm import ZoneMinder
|
||||||
|
|
||||||
from homeassistant.components.binary_sensor import (
|
from homeassistant.components.binary_sensor import (
|
||||||
DEVICE_CLASS_CONNECTIVITY,
|
DEVICE_CLASS_CONNECTIVITY,
|
||||||
BinarySensorEntity,
|
BinarySensorEntity,
|
||||||
)
|
)
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.entity import Entity
|
||||||
|
|
||||||
from . import DOMAIN as ZONEMINDER_DOMAIN
|
from .common import get_client_from_data
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_platform(hass, config, add_entities, discovery_info=None):
|
async def async_setup_entry(
|
||||||
"""Set up the ZoneMinder binary sensor platform."""
|
hass: HomeAssistant,
|
||||||
sensors = []
|
config_entry: ConfigEntry,
|
||||||
for host_name, zm_client in hass.data[ZONEMINDER_DOMAIN].items():
|
async_add_entities: Callable[[List[Entity], Optional[bool]], None],
|
||||||
sensors.append(ZMAvailabilitySensor(host_name, zm_client))
|
) -> None:
|
||||||
add_entities(sensors)
|
"""Set up the sensor config entry."""
|
||||||
return True
|
zm_client = get_client_from_data(hass, config_entry.unique_id)
|
||||||
|
async_add_entities([ZMAvailabilitySensor(zm_client, config_entry)])
|
||||||
|
|
||||||
|
|
||||||
class ZMAvailabilitySensor(BinarySensorEntity):
|
class ZMAvailabilitySensor(BinarySensorEntity):
|
||||||
"""Representation of the availability of ZoneMinder as a binary sensor."""
|
"""Representation of the availability of ZoneMinder as a binary sensor."""
|
||||||
|
|
||||||
def __init__(self, host_name, client):
|
def __init__(self, client: ZoneMinder, config_entry: ConfigEntry):
|
||||||
"""Initialize availability sensor."""
|
"""Initialize availability sensor."""
|
||||||
self._state = None
|
self._state = None
|
||||||
self._name = host_name
|
self._name = config_entry.unique_id
|
||||||
self._client = client
|
self._client = client
|
||||||
|
self._config_entry = config_entry
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unique_id(self) -> Optional[str]:
|
||||||
|
"""Return a unique ID."""
|
||||||
|
return f"{self._config_entry.unique_id}_availability"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
"""Support for ZoneMinder camera streaming."""
|
"""Support for ZoneMinder camera streaming."""
|
||||||
import logging
|
import logging
|
||||||
|
from typing import Callable, List, Optional
|
||||||
|
|
||||||
|
from zoneminder.monitor import Monitor
|
||||||
|
|
||||||
from homeassistant.components.mjpeg.camera import (
|
from homeassistant.components.mjpeg.camera import (
|
||||||
CONF_MJPEG_URL,
|
CONF_MJPEG_URL,
|
||||||
@ -7,9 +10,12 @@ from homeassistant.components.mjpeg.camera import (
|
|||||||
MjpegCamera,
|
MjpegCamera,
|
||||||
filter_urllib3_logging,
|
filter_urllib3_logging,
|
||||||
)
|
)
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import CONF_NAME, CONF_VERIFY_SSL
|
from homeassistant.const import CONF_NAME, CONF_VERIFY_SSL
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.entity import Entity
|
||||||
|
|
||||||
from . import DOMAIN as ZONEMINDER_DOMAIN
|
from .common import get_client_from_data
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -17,23 +23,28 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||||
"""Set up the ZoneMinder cameras."""
|
"""Set up the ZoneMinder cameras."""
|
||||||
filter_urllib3_logging()
|
filter_urllib3_logging()
|
||||||
cameras = []
|
|
||||||
for zm_client in hass.data[ZONEMINDER_DOMAIN].values():
|
|
||||||
monitors = zm_client.get_monitors()
|
|
||||||
if not monitors:
|
|
||||||
_LOGGER.warning("Could not fetch monitors from ZoneMinder host: %s")
|
|
||||||
return
|
|
||||||
|
|
||||||
for monitor in monitors:
|
|
||||||
_LOGGER.info("Initializing camera %s", monitor.id)
|
async def async_setup_entry(
|
||||||
cameras.append(ZoneMinderCamera(monitor, zm_client.verify_ssl))
|
hass: HomeAssistant,
|
||||||
add_entities(cameras)
|
config_entry: ConfigEntry,
|
||||||
|
async_add_entities: Callable[[List[Entity], Optional[bool]], None],
|
||||||
|
) -> None:
|
||||||
|
"""Set up the sensor config entry."""
|
||||||
|
zm_client = get_client_from_data(hass, config_entry.unique_id)
|
||||||
|
|
||||||
|
async_add_entities(
|
||||||
|
[
|
||||||
|
ZoneMinderCamera(monitor, zm_client.verify_ssl, config_entry)
|
||||||
|
for monitor in await hass.async_add_job(zm_client.get_monitors)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ZoneMinderCamera(MjpegCamera):
|
class ZoneMinderCamera(MjpegCamera):
|
||||||
"""Representation of a ZoneMinder Monitor Stream."""
|
"""Representation of a ZoneMinder Monitor Stream."""
|
||||||
|
|
||||||
def __init__(self, monitor, verify_ssl):
|
def __init__(self, monitor: Monitor, verify_ssl: bool, config_entry: ConfigEntry):
|
||||||
"""Initialize as a subclass of MjpegCamera."""
|
"""Initialize as a subclass of MjpegCamera."""
|
||||||
device_info = {
|
device_info = {
|
||||||
CONF_NAME: monitor.name,
|
CONF_NAME: monitor.name,
|
||||||
@ -45,6 +56,12 @@ class ZoneMinderCamera(MjpegCamera):
|
|||||||
self._is_recording = None
|
self._is_recording = None
|
||||||
self._is_available = None
|
self._is_available = None
|
||||||
self._monitor = monitor
|
self._monitor = monitor
|
||||||
|
self._config_entry = config_entry
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unique_id(self) -> Optional[str]:
|
||||||
|
"""Return a unique ID."""
|
||||||
|
return f"{self._config_entry.unique_id}_{self._monitor.id}_camera"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def should_poll(self):
|
def should_poll(self):
|
||||||
|
110
homeassistant/components/zoneminder/common.py
Normal file
110
homeassistant/components/zoneminder/common.py
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
"""Common code for the ZoneMinder component."""
|
||||||
|
from enum import Enum
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from zoneminder.zm import ZoneMinder
|
||||||
|
|
||||||
|
from homeassistant.const import (
|
||||||
|
CONF_HOST,
|
||||||
|
CONF_PASSWORD,
|
||||||
|
CONF_PATH,
|
||||||
|
CONF_SSL,
|
||||||
|
CONF_USERNAME,
|
||||||
|
CONF_VERIFY_SSL,
|
||||||
|
)
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
from . import const
|
||||||
|
|
||||||
|
|
||||||
|
def prime_domain_data(hass: HomeAssistant) -> None:
|
||||||
|
"""Prime the data structures."""
|
||||||
|
hass.data.setdefault(const.DOMAIN, {})
|
||||||
|
|
||||||
|
|
||||||
|
def prime_platform_configs(hass: HomeAssistant, domain: str) -> None:
|
||||||
|
"""Prime the data structures."""
|
||||||
|
prime_domain_data(hass)
|
||||||
|
hass.data[const.DOMAIN].setdefault(const.PLATFORM_CONFIGS, {})
|
||||||
|
hass.data[const.DOMAIN][const.PLATFORM_CONFIGS].setdefault(domain, [])
|
||||||
|
|
||||||
|
|
||||||
|
def set_platform_configs(hass: HomeAssistant, domain: str, configs: List[dict]) -> None:
|
||||||
|
"""Set platform configs."""
|
||||||
|
prime_platform_configs(hass, domain)
|
||||||
|
hass.data[const.DOMAIN][const.PLATFORM_CONFIGS][domain] = configs
|
||||||
|
|
||||||
|
|
||||||
|
def get_platform_configs(hass: HomeAssistant, domain: str) -> List[dict]:
|
||||||
|
"""Get platform configs."""
|
||||||
|
prime_platform_configs(hass, domain)
|
||||||
|
return hass.data[const.DOMAIN][const.PLATFORM_CONFIGS][domain]
|
||||||
|
|
||||||
|
|
||||||
|
def prime_config_data(hass: HomeAssistant, unique_id: str) -> None:
|
||||||
|
"""Prime the data structures."""
|
||||||
|
prime_domain_data(hass)
|
||||||
|
hass.data[const.DOMAIN].setdefault(const.CONFIG_DATA, {})
|
||||||
|
hass.data[const.DOMAIN][const.CONFIG_DATA].setdefault(unique_id, {})
|
||||||
|
|
||||||
|
|
||||||
|
def set_client_to_data(hass: HomeAssistant, unique_id: str, client: ZoneMinder) -> None:
|
||||||
|
"""Put a ZoneMinder client in the Home Assistant data."""
|
||||||
|
prime_config_data(hass, unique_id)
|
||||||
|
hass.data[const.DOMAIN][const.CONFIG_DATA][unique_id][const.API_CLIENT] = client
|
||||||
|
|
||||||
|
|
||||||
|
def is_client_in_data(hass: HomeAssistant, unique_id: str) -> bool:
|
||||||
|
"""Check if ZoneMinder client is in the Home Assistant data."""
|
||||||
|
prime_config_data(hass, unique_id)
|
||||||
|
return const.API_CLIENT in hass.data[const.DOMAIN][const.CONFIG_DATA][unique_id]
|
||||||
|
|
||||||
|
|
||||||
|
def get_client_from_data(hass: HomeAssistant, unique_id: str) -> ZoneMinder:
|
||||||
|
"""Get a ZoneMinder client from the Home Assistant data."""
|
||||||
|
prime_config_data(hass, unique_id)
|
||||||
|
return hass.data[const.DOMAIN][const.CONFIG_DATA][unique_id][const.API_CLIENT]
|
||||||
|
|
||||||
|
|
||||||
|
def del_client_from_data(hass: HomeAssistant, unique_id: str) -> None:
|
||||||
|
"""Delete a ZoneMinder client from the Home Assistant data."""
|
||||||
|
prime_config_data(hass, unique_id)
|
||||||
|
del hass.data[const.DOMAIN][const.CONFIG_DATA][unique_id][const.API_CLIENT]
|
||||||
|
|
||||||
|
|
||||||
|
def create_client_from_config(conf: dict) -> ZoneMinder:
|
||||||
|
"""Create a new ZoneMinder client from a config."""
|
||||||
|
protocol = "https" if conf[CONF_SSL] else "http"
|
||||||
|
|
||||||
|
host_name = conf[CONF_HOST]
|
||||||
|
server_origin = f"{protocol}://{host_name}"
|
||||||
|
|
||||||
|
return ZoneMinder(
|
||||||
|
server_origin,
|
||||||
|
conf.get(CONF_USERNAME),
|
||||||
|
conf.get(CONF_PASSWORD),
|
||||||
|
conf.get(CONF_PATH),
|
||||||
|
conf.get(const.CONF_PATH_ZMS),
|
||||||
|
conf.get(CONF_VERIFY_SSL),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ClientAvailabilityResult(Enum):
|
||||||
|
"""Client availability test result."""
|
||||||
|
|
||||||
|
AVAILABLE = "available"
|
||||||
|
ERROR_AUTH_FAIL = "auth_fail"
|
||||||
|
ERROR_CONNECTION_ERROR = "connection_error"
|
||||||
|
|
||||||
|
|
||||||
|
async def async_test_client_availability(
|
||||||
|
hass: HomeAssistant, client: ZoneMinder
|
||||||
|
) -> ClientAvailabilityResult:
|
||||||
|
"""Test the availability of a ZoneMinder client."""
|
||||||
|
try:
|
||||||
|
if await hass.async_add_job(client.login):
|
||||||
|
return ClientAvailabilityResult.AVAILABLE
|
||||||
|
return ClientAvailabilityResult.ERROR_AUTH_FAIL
|
||||||
|
except requests.exceptions.ConnectionError:
|
||||||
|
return ClientAvailabilityResult.ERROR_CONNECTION_ERROR
|
99
homeassistant/components/zoneminder/config_flow.py
Normal file
99
homeassistant/components/zoneminder/config_flow.py
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
"""ZoneMinder config flow."""
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant import config_entries
|
||||||
|
from homeassistant.const import (
|
||||||
|
CONF_HOST,
|
||||||
|
CONF_PASSWORD,
|
||||||
|
CONF_PATH,
|
||||||
|
CONF_SOURCE,
|
||||||
|
CONF_SSL,
|
||||||
|
CONF_USERNAME,
|
||||||
|
CONF_VERIFY_SSL,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .common import (
|
||||||
|
ClientAvailabilityResult,
|
||||||
|
async_test_client_availability,
|
||||||
|
create_client_from_config,
|
||||||
|
)
|
||||||
|
from .const import (
|
||||||
|
CONF_PATH_ZMS,
|
||||||
|
DEFAULT_PATH,
|
||||||
|
DEFAULT_PATH_ZMS,
|
||||||
|
DEFAULT_SSL,
|
||||||
|
DEFAULT_VERIFY_SSL,
|
||||||
|
)
|
||||||
|
from .const import DOMAIN # pylint: disable=unused-import
|
||||||
|
|
||||||
|
|
||||||
|
class ZoneminderFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||||
|
"""Flow handler for zoneminder integration."""
|
||||||
|
|
||||||
|
VERSION = 1
|
||||||
|
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
|
||||||
|
|
||||||
|
async def async_step_import(self, config: dict):
|
||||||
|
"""Handle a flow initialized by import."""
|
||||||
|
return await self.async_step_finish(
|
||||||
|
{**config, **{CONF_SOURCE: config_entries.SOURCE_IMPORT}}
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_step_user(self, user_input: dict = None):
|
||||||
|
"""Handle user step."""
|
||||||
|
user_input = user_input or {}
|
||||||
|
errors = {}
|
||||||
|
|
||||||
|
if user_input:
|
||||||
|
zm_client = create_client_from_config(user_input)
|
||||||
|
result = await async_test_client_availability(self.hass, zm_client)
|
||||||
|
if result == ClientAvailabilityResult.AVAILABLE:
|
||||||
|
return await self.async_step_finish(user_input)
|
||||||
|
|
||||||
|
errors["base"] = result.value
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id=config_entries.SOURCE_USER,
|
||||||
|
data_schema=vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required(CONF_HOST, default=user_input.get(CONF_HOST)): str,
|
||||||
|
vol.Optional(
|
||||||
|
CONF_USERNAME, default=user_input.get(CONF_USERNAME)
|
||||||
|
): str,
|
||||||
|
vol.Optional(
|
||||||
|
CONF_PASSWORD, default=user_input.get(CONF_PASSWORD)
|
||||||
|
): str,
|
||||||
|
vol.Optional(
|
||||||
|
CONF_PATH, default=user_input.get(CONF_PATH, DEFAULT_PATH)
|
||||||
|
): str,
|
||||||
|
vol.Optional(
|
||||||
|
CONF_PATH_ZMS,
|
||||||
|
default=user_input.get(CONF_PATH_ZMS, DEFAULT_PATH_ZMS),
|
||||||
|
): str,
|
||||||
|
vol.Optional(
|
||||||
|
CONF_SSL, default=user_input.get(CONF_SSL, DEFAULT_SSL)
|
||||||
|
): bool,
|
||||||
|
vol.Optional(
|
||||||
|
CONF_VERIFY_SSL,
|
||||||
|
default=user_input.get(CONF_VERIFY_SSL, DEFAULT_VERIFY_SSL),
|
||||||
|
): bool,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
errors=errors,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_step_finish(self, config: dict):
|
||||||
|
"""Finish config flow."""
|
||||||
|
zm_client = create_client_from_config(config)
|
||||||
|
hostname = urlparse(zm_client.get_zms_url()).hostname
|
||||||
|
result = await async_test_client_availability(self.hass, zm_client)
|
||||||
|
|
||||||
|
if result != ClientAvailabilityResult.AVAILABLE:
|
||||||
|
return self.async_abort(reason=str(result.value))
|
||||||
|
|
||||||
|
await self.async_set_unique_id(hostname)
|
||||||
|
self._abort_if_unique_id_configured(config)
|
||||||
|
|
||||||
|
return self.async_create_entry(title=hostname, data=config)
|
14
homeassistant/components/zoneminder/const.py
Normal file
14
homeassistant/components/zoneminder/const.py
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
"""Constants for zoneminder component."""
|
||||||
|
|
||||||
|
CONF_PATH_ZMS = "path_zms"
|
||||||
|
|
||||||
|
DEFAULT_PATH = "/zm/"
|
||||||
|
DEFAULT_PATH_ZMS = "/zm/cgi-bin/nph-zms"
|
||||||
|
DEFAULT_SSL = False
|
||||||
|
DEFAULT_VERIFY_SSL = True
|
||||||
|
DOMAIN = "zoneminder"
|
||||||
|
SERVICE_SET_RUN_STATE = "set_run_state"
|
||||||
|
|
||||||
|
PLATFORM_CONFIGS = "platform_configs"
|
||||||
|
CONFIG_DATA = "config_data"
|
||||||
|
API_CLIENT = "api_client"
|
@ -1,7 +1,8 @@
|
|||||||
{
|
{
|
||||||
"domain": "zoneminder",
|
"domain": "zoneminder",
|
||||||
"name": "ZoneMinder",
|
"name": "ZoneMinder",
|
||||||
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/zoneminder",
|
"documentation": "https://www.home-assistant.io/integrations/zoneminder",
|
||||||
"requirements": ["zm-py==0.4.0"],
|
"requirements": ["zm-py==0.4.0"],
|
||||||
"codeowners": ["@rohankapoorcom"]
|
"codeowners": ["@rohankapoorcom", "@vangorra"]
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,19 @@
|
|||||||
"""Support for ZoneMinder sensors."""
|
"""Support for ZoneMinder sensors."""
|
||||||
import logging
|
import logging
|
||||||
|
from typing import Callable, List, Optional
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
from zoneminder.monitor import TimePeriod
|
from zoneminder.monitor import Monitor, TimePeriod
|
||||||
|
from zoneminder.zm import ZoneMinder
|
||||||
|
|
||||||
from homeassistant.components.sensor import PLATFORM_SCHEMA
|
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, PLATFORM_SCHEMA
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import CONF_MONITORED_CONDITIONS
|
from homeassistant.const import CONF_MONITORED_CONDITIONS
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
|
|
||||||
from . import DOMAIN as ZONEMINDER_DOMAIN
|
from .common import get_client_from_data, get_platform_configs
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -37,35 +41,50 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
async def async_setup_entry(
|
||||||
"""Set up the ZoneMinder sensor platform."""
|
hass: HomeAssistant,
|
||||||
include_archived = config.get(CONF_INCLUDE_ARCHIVED)
|
config_entry: ConfigEntry,
|
||||||
|
async_add_entities: Callable[[List[Entity], Optional[bool]], None],
|
||||||
|
) -> None:
|
||||||
|
"""Set up the sensor config entry."""
|
||||||
|
zm_client = get_client_from_data(hass, config_entry.unique_id)
|
||||||
|
monitors = await hass.async_add_job(zm_client.get_monitors)
|
||||||
|
|
||||||
|
if not monitors:
|
||||||
|
_LOGGER.warning("Did not fetch any monitors from ZoneMinder")
|
||||||
|
|
||||||
sensors = []
|
sensors = []
|
||||||
for zm_client in hass.data[ZONEMINDER_DOMAIN].values():
|
|
||||||
monitors = zm_client.get_monitors()
|
|
||||||
if not monitors:
|
|
||||||
_LOGGER.warning("Could not fetch any monitors from ZoneMinder")
|
|
||||||
|
|
||||||
for monitor in monitors:
|
for monitor in monitors:
|
||||||
sensors.append(ZMSensorMonitors(monitor))
|
sensors.append(ZMSensorMonitors(monitor, config_entry))
|
||||||
|
|
||||||
|
for config in get_platform_configs(hass, SENSOR_DOMAIN):
|
||||||
|
include_archived = config.get(CONF_INCLUDE_ARCHIVED)
|
||||||
|
|
||||||
for sensor in config[CONF_MONITORED_CONDITIONS]:
|
for sensor in config[CONF_MONITORED_CONDITIONS]:
|
||||||
sensors.append(ZMSensorEvents(monitor, include_archived, sensor))
|
sensors.append(
|
||||||
|
ZMSensorEvents(monitor, include_archived, sensor, config_entry)
|
||||||
|
)
|
||||||
|
|
||||||
sensors.append(ZMSensorRunState(zm_client))
|
sensors.append(ZMSensorRunState(zm_client, config_entry))
|
||||||
add_entities(sensors)
|
|
||||||
|
async_add_entities(sensors, True)
|
||||||
|
|
||||||
|
|
||||||
class ZMSensorMonitors(Entity):
|
class ZMSensorMonitors(Entity):
|
||||||
"""Get the status of each ZoneMinder monitor."""
|
"""Get the status of each ZoneMinder monitor."""
|
||||||
|
|
||||||
def __init__(self, monitor):
|
def __init__(self, monitor: Monitor, config_entry: ConfigEntry):
|
||||||
"""Initialize monitor sensor."""
|
"""Initialize monitor sensor."""
|
||||||
self._monitor = monitor
|
self._monitor = monitor
|
||||||
|
self._config_entry = config_entry
|
||||||
self._state = None
|
self._state = None
|
||||||
self._is_available = None
|
self._is_available = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unique_id(self) -> Optional[str]:
|
||||||
|
"""Return a unique ID."""
|
||||||
|
return f"{self._config_entry.unique_id}_{self._monitor.id}_status"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
"""Return the name of the sensor."""
|
"""Return the name of the sensor."""
|
||||||
@ -94,14 +113,26 @@ class ZMSensorMonitors(Entity):
|
|||||||
class ZMSensorEvents(Entity):
|
class ZMSensorEvents(Entity):
|
||||||
"""Get the number of events for each monitor."""
|
"""Get the number of events for each monitor."""
|
||||||
|
|
||||||
def __init__(self, monitor, include_archived, sensor_type):
|
def __init__(
|
||||||
|
self,
|
||||||
|
monitor: Monitor,
|
||||||
|
include_archived: bool,
|
||||||
|
sensor_type: str,
|
||||||
|
config_entry: ConfigEntry,
|
||||||
|
):
|
||||||
"""Initialize event sensor."""
|
"""Initialize event sensor."""
|
||||||
|
|
||||||
self._monitor = monitor
|
self._monitor = monitor
|
||||||
self._include_archived = include_archived
|
self._include_archived = include_archived
|
||||||
self.time_period = TimePeriod.get_time_period(sensor_type)
|
self.time_period = TimePeriod.get_time_period(sensor_type)
|
||||||
|
self._config_entry = config_entry
|
||||||
self._state = None
|
self._state = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unique_id(self) -> Optional[str]:
|
||||||
|
"""Return a unique ID."""
|
||||||
|
return f"{self._config_entry.unique_id}_{self._monitor.id}_{self.time_period.value}_{self._include_archived}_events"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
"""Return the name of the sensor."""
|
"""Return the name of the sensor."""
|
||||||
@ -125,11 +156,17 @@ class ZMSensorEvents(Entity):
|
|||||||
class ZMSensorRunState(Entity):
|
class ZMSensorRunState(Entity):
|
||||||
"""Get the ZoneMinder run state."""
|
"""Get the ZoneMinder run state."""
|
||||||
|
|
||||||
def __init__(self, client):
|
def __init__(self, client: ZoneMinder, config_entry: ConfigEntry):
|
||||||
"""Initialize run state sensor."""
|
"""Initialize run state sensor."""
|
||||||
self._state = None
|
self._state = None
|
||||||
self._is_available = None
|
self._is_available = None
|
||||||
self._client = client
|
self._client = client
|
||||||
|
self._config_entry = config_entry
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unique_id(self) -> Optional[str]:
|
||||||
|
"""Return a unique ID."""
|
||||||
|
return f"{self._config_entry.unique_id}_runstate"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
set_run_state:
|
set_run_state:
|
||||||
description: Set the ZoneMinder run state
|
description: "Set the ZoneMinder run state"
|
||||||
fields:
|
fields:
|
||||||
|
id:
|
||||||
|
description: "The host name or IP address of the ZoneMinder instance."
|
||||||
|
example: "10.10.0.2"
|
||||||
name:
|
name:
|
||||||
description: The string name of the ZoneMinder run state to set as active.
|
description: "The string name of the ZoneMinder run state to set as active."
|
||||||
example: "Home"
|
example: "Home"
|
||||||
|
28
homeassistant/components/zoneminder/strings.json
Normal file
28
homeassistant/components/zoneminder/strings.json
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"flow_title": "ZoneMinder",
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"title": "Add ZoneMinder Server.",
|
||||||
|
"data": {
|
||||||
|
"host": "Host and Port (ex 10.10.0.4:8010)",
|
||||||
|
"username": "[%key:common::config_flow::data::username%]",
|
||||||
|
"password": "[%key:common::config_flow::data::password%]",
|
||||||
|
"path": "ZM Path",
|
||||||
|
"path_zms": "ZMS Path",
|
||||||
|
"ssl": "Use SSL for connections to ZoneMinder",
|
||||||
|
"verify_ssl": "Verify SSL Certificate"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"abort": {
|
||||||
|
"auth_fail": "Username or password is incorrect.",
|
||||||
|
"connection_error": "Failed to connect to a ZoneMinder server."
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"auth_fail": "Username or password is incorrect.",
|
||||||
|
"connection_error": "Failed to connect to a ZoneMinder server."
|
||||||
|
},
|
||||||
|
"create_entry": { "default": "ZoneMinder server added." }
|
||||||
|
}
|
||||||
|
}
|
@ -1,41 +1,61 @@
|
|||||||
"""Support for ZoneMinder switches."""
|
"""Support for ZoneMinder switches."""
|
||||||
import logging
|
import logging
|
||||||
|
from typing import Callable, List, Optional
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
from zoneminder.monitor import MonitorState
|
from zoneminder.monitor import Monitor, MonitorState
|
||||||
|
|
||||||
from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity
|
from homeassistant.components.switch import (
|
||||||
|
DOMAIN as SWITCH_DOMAIN,
|
||||||
|
PLATFORM_SCHEMA,
|
||||||
|
SwitchEntity,
|
||||||
|
)
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import CONF_COMMAND_OFF, CONF_COMMAND_ON
|
from homeassistant.const import CONF_COMMAND_OFF, CONF_COMMAND_ON
|
||||||
import homeassistant.helpers.config_validation as cv
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.entity import Entity
|
||||||
|
|
||||||
from . import DOMAIN as ZONEMINDER_DOMAIN
|
from .common import get_client_from_data, get_platform_configs
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
MONITOR_STATES = {
|
||||||
|
MonitorState[name].value: MonitorState[name]
|
||||||
|
for name in dir(MonitorState)
|
||||||
|
if not name.startswith("_")
|
||||||
|
}
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||||
{
|
{
|
||||||
vol.Required(CONF_COMMAND_ON): cv.string,
|
vol.Required(CONF_COMMAND_ON): vol.All(vol.In(MONITOR_STATES.keys())),
|
||||||
vol.Required(CONF_COMMAND_OFF): cv.string,
|
vol.Required(CONF_COMMAND_OFF): vol.All(vol.In(MONITOR_STATES.keys())),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
async def async_setup_entry(
|
||||||
"""Set up the ZoneMinder switch platform."""
|
hass: HomeAssistant,
|
||||||
|
config_entry: ConfigEntry,
|
||||||
|
async_add_entities: Callable[[List[Entity], Optional[bool]], None],
|
||||||
|
) -> None:
|
||||||
|
"""Set up the sensor config entry."""
|
||||||
|
zm_client = get_client_from_data(hass, config_entry.unique_id)
|
||||||
|
monitors = await hass.async_add_job(zm_client.get_monitors)
|
||||||
|
|
||||||
on_state = MonitorState(config.get(CONF_COMMAND_ON))
|
|
||||||
off_state = MonitorState(config.get(CONF_COMMAND_OFF))
|
|
||||||
|
|
||||||
switches = []
|
|
||||||
for zm_client in hass.data[ZONEMINDER_DOMAIN].values():
|
|
||||||
monitors = zm_client.get_monitors()
|
|
||||||
if not monitors:
|
if not monitors:
|
||||||
_LOGGER.warning("Could not fetch monitors from ZoneMinder")
|
_LOGGER.warning("Could not fetch monitors from ZoneMinder")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
switches = []
|
||||||
for monitor in monitors:
|
for monitor in monitors:
|
||||||
switches.append(ZMSwitchMonitors(monitor, on_state, off_state))
|
for config in get_platform_configs(hass, SWITCH_DOMAIN):
|
||||||
add_entities(switches)
|
on_state = MONITOR_STATES[config[CONF_COMMAND_ON]]
|
||||||
|
off_state = MONITOR_STATES[config[CONF_COMMAND_OFF]]
|
||||||
|
|
||||||
|
switches.append(
|
||||||
|
ZMSwitchMonitors(monitor, on_state, off_state, config_entry)
|
||||||
|
)
|
||||||
|
|
||||||
|
async_add_entities(switches, True)
|
||||||
|
|
||||||
|
|
||||||
class ZMSwitchMonitors(SwitchEntity):
|
class ZMSwitchMonitors(SwitchEntity):
|
||||||
@ -43,13 +63,25 @@ class ZMSwitchMonitors(SwitchEntity):
|
|||||||
|
|
||||||
icon = "mdi:record-rec"
|
icon = "mdi:record-rec"
|
||||||
|
|
||||||
def __init__(self, monitor, on_state, off_state):
|
def __init__(
|
||||||
|
self,
|
||||||
|
monitor: Monitor,
|
||||||
|
on_state: MonitorState,
|
||||||
|
off_state: MonitorState,
|
||||||
|
config_entry: ConfigEntry,
|
||||||
|
):
|
||||||
"""Initialize the switch."""
|
"""Initialize the switch."""
|
||||||
self._monitor = monitor
|
self._monitor = monitor
|
||||||
self._on_state = on_state
|
self._on_state = on_state
|
||||||
self._off_state = off_state
|
self._off_state = off_state
|
||||||
|
self._config_entry = config_entry
|
||||||
self._state = None
|
self._state = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unique_id(self) -> Optional[str]:
|
||||||
|
"""Return a unique ID."""
|
||||||
|
return f"{self._config_entry.unique_id}_{self._monitor.id}_switch_{self._on_state.value}_{self._off_state.value}"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
"""Return the name of the switch."""
|
"""Return the name of the switch."""
|
||||||
|
@ -214,5 +214,6 @@ FLOWS = [
|
|||||||
"yeelight",
|
"yeelight",
|
||||||
"zerproc",
|
"zerproc",
|
||||||
"zha",
|
"zha",
|
||||||
|
"zoneminder",
|
||||||
"zwave"
|
"zwave"
|
||||||
]
|
]
|
||||||
|
@ -1081,3 +1081,6 @@ zigpy-znp==0.1.1
|
|||||||
|
|
||||||
# homeassistant.components.zha
|
# homeassistant.components.zha
|
||||||
zigpy==0.23.2
|
zigpy==0.23.2
|
||||||
|
|
||||||
|
# homeassistant.components.zoneminder
|
||||||
|
zm-py==0.4.0
|
||||||
|
1
tests/components/zoneminder/__init__.py
Normal file
1
tests/components/zoneminder/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
"""Tests for the zoneminder component."""
|
65
tests/components/zoneminder/test_binary_sensor.py
Normal file
65
tests/components/zoneminder/test_binary_sensor.py
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
"""Binary sensor tests."""
|
||||||
|
from zoneminder.zm import ZoneMinder
|
||||||
|
|
||||||
|
from homeassistant.components.zoneminder import const
|
||||||
|
from homeassistant.config import async_process_ha_core_config
|
||||||
|
from homeassistant.const import (
|
||||||
|
ATTR_ENTITY_ID,
|
||||||
|
CONF_HOST,
|
||||||
|
CONF_PASSWORD,
|
||||||
|
CONF_PATH,
|
||||||
|
CONF_SSL,
|
||||||
|
CONF_USERNAME,
|
||||||
|
CONF_VERIFY_SSL,
|
||||||
|
)
|
||||||
|
from homeassistant.core import DOMAIN as HASS_DOMAIN, HomeAssistant
|
||||||
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
|
from tests.async_mock import MagicMock, patch
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
|
||||||
|
async def test_async_setup_entry(hass: HomeAssistant) -> None:
|
||||||
|
"""Test setup of binary sensor entities."""
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.zoneminder.common.ZoneMinder", autospec=ZoneMinder
|
||||||
|
) as zoneminder_mock:
|
||||||
|
zm_client: ZoneMinder = MagicMock(spec=ZoneMinder)
|
||||||
|
zm_client.get_zms_url.return_value = "http://host1/path_zms1"
|
||||||
|
zm_client.login.return_value = True
|
||||||
|
zm_client.is_available = True
|
||||||
|
|
||||||
|
zoneminder_mock.return_value = zm_client
|
||||||
|
|
||||||
|
config_entry = MockConfigEntry(
|
||||||
|
domain=const.DOMAIN,
|
||||||
|
unique_id="host1",
|
||||||
|
data={
|
||||||
|
CONF_HOST: "host1",
|
||||||
|
CONF_USERNAME: "username1",
|
||||||
|
CONF_PASSWORD: "password1",
|
||||||
|
CONF_PATH: "path1",
|
||||||
|
const.CONF_PATH_ZMS: "path_zms1",
|
||||||
|
CONF_SSL: False,
|
||||||
|
CONF_VERIFY_SSL: True,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
config_entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
await async_process_ha_core_config(hass, {})
|
||||||
|
await async_setup_component(hass, HASS_DOMAIN, {})
|
||||||
|
await async_setup_component(hass, const.DOMAIN, {})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
HASS_DOMAIN, "update_entity", {ATTR_ENTITY_ID: "binary_sensor.host1"}
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert hass.states.get("binary_sensor.host1").state == "on"
|
||||||
|
|
||||||
|
zm_client.is_available = False
|
||||||
|
await hass.services.async_call(
|
||||||
|
HASS_DOMAIN, "update_entity", {ATTR_ENTITY_ID: "binary_sensor.host1"}
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert hass.states.get("binary_sensor.host1").state == "off"
|
89
tests/components/zoneminder/test_camera.py
Normal file
89
tests/components/zoneminder/test_camera.py
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
"""Binary sensor tests."""
|
||||||
|
from zoneminder.monitor import Monitor
|
||||||
|
from zoneminder.zm import ZoneMinder
|
||||||
|
|
||||||
|
from homeassistant.components.zoneminder import const
|
||||||
|
from homeassistant.config import async_process_ha_core_config
|
||||||
|
from homeassistant.const import (
|
||||||
|
ATTR_ENTITY_ID,
|
||||||
|
CONF_HOST,
|
||||||
|
CONF_PASSWORD,
|
||||||
|
CONF_PATH,
|
||||||
|
CONF_SSL,
|
||||||
|
CONF_USERNAME,
|
||||||
|
CONF_VERIFY_SSL,
|
||||||
|
)
|
||||||
|
from homeassistant.core import DOMAIN as HASS_DOMAIN, HomeAssistant
|
||||||
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
|
from tests.async_mock import MagicMock, patch
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
|
||||||
|
async def test_async_setup_entry(hass: HomeAssistant) -> None:
|
||||||
|
"""Test setup of camera entities."""
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.zoneminder.common.ZoneMinder", autospec=ZoneMinder
|
||||||
|
) as zoneminder_mock:
|
||||||
|
monitor1 = MagicMock(spec=Monitor)
|
||||||
|
monitor1.name = "monitor1"
|
||||||
|
monitor1.mjpeg_image_url = "mjpeg_image_url1"
|
||||||
|
monitor1.still_image_url = "still_image_url1"
|
||||||
|
monitor1.is_recording = True
|
||||||
|
monitor1.is_available = True
|
||||||
|
|
||||||
|
monitor2 = MagicMock(spec=Monitor)
|
||||||
|
monitor2.name = "monitor2"
|
||||||
|
monitor2.mjpeg_image_url = "mjpeg_image_url2"
|
||||||
|
monitor2.still_image_url = "still_image_url2"
|
||||||
|
monitor2.is_recording = False
|
||||||
|
monitor2.is_available = False
|
||||||
|
|
||||||
|
zm_client: ZoneMinder = MagicMock(spec=ZoneMinder)
|
||||||
|
zm_client.get_zms_url.return_value = "http://host1/path_zms1"
|
||||||
|
zm_client.login.return_value = True
|
||||||
|
zm_client.get_monitors.return_value = [monitor1, monitor2]
|
||||||
|
|
||||||
|
zoneminder_mock.return_value = zm_client
|
||||||
|
|
||||||
|
config_entry = MockConfigEntry(
|
||||||
|
domain=const.DOMAIN,
|
||||||
|
unique_id="host1",
|
||||||
|
data={
|
||||||
|
CONF_HOST: "host1",
|
||||||
|
CONF_USERNAME: "username1",
|
||||||
|
CONF_PASSWORD: "password1",
|
||||||
|
CONF_PATH: "path1",
|
||||||
|
const.CONF_PATH_ZMS: "path_zms1",
|
||||||
|
CONF_SSL: False,
|
||||||
|
CONF_VERIFY_SSL: True,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
config_entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
await async_process_ha_core_config(hass, {})
|
||||||
|
await async_setup_component(hass, HASS_DOMAIN, {})
|
||||||
|
await async_setup_component(hass, const.DOMAIN, {})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
HASS_DOMAIN, "update_entity", {ATTR_ENTITY_ID: "camera.monitor1"}
|
||||||
|
)
|
||||||
|
await hass.services.async_call(
|
||||||
|
HASS_DOMAIN, "update_entity", {ATTR_ENTITY_ID: "camera.monitor2"}
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert hass.states.get("camera.monitor1").state == "recording"
|
||||||
|
assert hass.states.get("camera.monitor2").state == "unavailable"
|
||||||
|
|
||||||
|
monitor1.is_recording = False
|
||||||
|
monitor2.is_recording = True
|
||||||
|
await hass.services.async_call(
|
||||||
|
HASS_DOMAIN, "update_entity", {ATTR_ENTITY_ID: "camera.monitor1"}
|
||||||
|
)
|
||||||
|
await hass.services.async_call(
|
||||||
|
HASS_DOMAIN, "update_entity", {ATTR_ENTITY_ID: "camera.monitor2"}
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert hass.states.get("camera.monitor1").state == "idle"
|
||||||
|
assert hass.states.get("camera.monitor2").state == "unavailable"
|
119
tests/components/zoneminder/test_config_flow.py
Normal file
119
tests/components/zoneminder/test_config_flow.py
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
"""Config flow tests."""
|
||||||
|
import requests
|
||||||
|
from zoneminder.zm import ZoneMinder
|
||||||
|
|
||||||
|
from homeassistant import config_entries
|
||||||
|
from homeassistant.components.zoneminder import ClientAvailabilityResult, const
|
||||||
|
from homeassistant.const import (
|
||||||
|
CONF_HOST,
|
||||||
|
CONF_PASSWORD,
|
||||||
|
CONF_PATH,
|
||||||
|
CONF_SOURCE,
|
||||||
|
CONF_SSL,
|
||||||
|
CONF_USERNAME,
|
||||||
|
CONF_VERIFY_SSL,
|
||||||
|
)
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
from tests.async_mock import MagicMock, patch
|
||||||
|
|
||||||
|
|
||||||
|
async def test_import(hass: HomeAssistant) -> None:
|
||||||
|
"""Test import from configuration yaml."""
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.zoneminder.common.ZoneMinder", autospec=ZoneMinder
|
||||||
|
) as zoneminder_mock:
|
||||||
|
conf_data = {
|
||||||
|
CONF_HOST: "host1",
|
||||||
|
CONF_USERNAME: "username1",
|
||||||
|
CONF_PASSWORD: "password1",
|
||||||
|
CONF_PATH: "path1",
|
||||||
|
const.CONF_PATH_ZMS: "path_zms1",
|
||||||
|
CONF_SSL: False,
|
||||||
|
CONF_VERIFY_SSL: True,
|
||||||
|
}
|
||||||
|
|
||||||
|
zm_client: ZoneMinder = MagicMock(spec=ZoneMinder)
|
||||||
|
zm_client.get_zms_url.return_value = "http://host1/path_zms1"
|
||||||
|
zoneminder_mock.return_value = zm_client
|
||||||
|
|
||||||
|
zm_client.login.return_value = False
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
const.DOMAIN,
|
||||||
|
context={"source": config_entries.SOURCE_IMPORT},
|
||||||
|
data=conf_data,
|
||||||
|
)
|
||||||
|
assert result
|
||||||
|
assert result["type"] == "abort"
|
||||||
|
assert result["reason"] == "auth_fail"
|
||||||
|
|
||||||
|
zm_client.login.return_value = True
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
const.DOMAIN,
|
||||||
|
context={"source": config_entries.SOURCE_IMPORT},
|
||||||
|
data=conf_data,
|
||||||
|
)
|
||||||
|
assert result
|
||||||
|
assert result["type"] == "create_entry"
|
||||||
|
assert result["data"] == {
|
||||||
|
**conf_data,
|
||||||
|
CONF_SOURCE: config_entries.SOURCE_IMPORT,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_user(hass: HomeAssistant) -> None:
|
||||||
|
"""Test user initiated creation."""
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.zoneminder.common.ZoneMinder", autospec=ZoneMinder
|
||||||
|
) as zoneminder_mock:
|
||||||
|
conf_data = {
|
||||||
|
CONF_HOST: "host1",
|
||||||
|
CONF_USERNAME: "username1",
|
||||||
|
CONF_PASSWORD: "password1",
|
||||||
|
CONF_PATH: "path1",
|
||||||
|
const.CONF_PATH_ZMS: "path_zms1",
|
||||||
|
CONF_SSL: False,
|
||||||
|
CONF_VERIFY_SSL: True,
|
||||||
|
}
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
const.DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||||
|
)
|
||||||
|
assert result
|
||||||
|
assert result["type"] == "form"
|
||||||
|
|
||||||
|
zm_client: ZoneMinder = MagicMock(spec=ZoneMinder)
|
||||||
|
zoneminder_mock.return_value = zm_client
|
||||||
|
|
||||||
|
zm_client.login.side_effect = requests.exceptions.ConnectionError()
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
conf_data,
|
||||||
|
)
|
||||||
|
assert result
|
||||||
|
assert result["type"] == "form"
|
||||||
|
assert result["errors"] == {
|
||||||
|
"base": ClientAvailabilityResult.ERROR_CONNECTION_ERROR.value
|
||||||
|
}
|
||||||
|
|
||||||
|
zm_client.login.side_effect = None
|
||||||
|
zm_client.login.return_value = False
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
conf_data,
|
||||||
|
)
|
||||||
|
assert result
|
||||||
|
assert result["type"] == "form"
|
||||||
|
assert result["errors"] == {
|
||||||
|
"base": ClientAvailabilityResult.ERROR_AUTH_FAIL.value
|
||||||
|
}
|
||||||
|
|
||||||
|
zm_client.login.return_value = True
|
||||||
|
zm_client.get_zms_url.return_value = "http://host1/path_zms1"
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
conf_data,
|
||||||
|
)
|
||||||
|
assert result
|
||||||
|
assert result["type"] == "create_entry"
|
||||||
|
assert result["data"] == conf_data
|
122
tests/components/zoneminder/test_init.py
Normal file
122
tests/components/zoneminder/test_init.py
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
"""Tests for init functions."""
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
from zoneminder.zm import ZoneMinder
|
||||||
|
|
||||||
|
from homeassistant import config_entries
|
||||||
|
from homeassistant.components.zoneminder import const
|
||||||
|
from homeassistant.components.zoneminder.common import is_client_in_data
|
||||||
|
from homeassistant.config_entries import (
|
||||||
|
ENTRY_STATE_LOADED,
|
||||||
|
ENTRY_STATE_NOT_LOADED,
|
||||||
|
ENTRY_STATE_SETUP_RETRY,
|
||||||
|
)
|
||||||
|
from homeassistant.const import (
|
||||||
|
ATTR_ID,
|
||||||
|
ATTR_NAME,
|
||||||
|
CONF_HOST,
|
||||||
|
CONF_PASSWORD,
|
||||||
|
CONF_PATH,
|
||||||
|
CONF_SOURCE,
|
||||||
|
CONF_SSL,
|
||||||
|
CONF_USERNAME,
|
||||||
|
CONF_VERIFY_SSL,
|
||||||
|
)
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.setup import async_setup_component
|
||||||
|
import homeassistant.util.dt as dt_util
|
||||||
|
|
||||||
|
from tests.async_mock import MagicMock, patch
|
||||||
|
from tests.common import async_fire_time_changed
|
||||||
|
|
||||||
|
|
||||||
|
async def test_no_yaml_config(hass: HomeAssistant) -> None:
|
||||||
|
"""Test empty yaml config."""
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.zoneminder.common.ZoneMinder", autospec=ZoneMinder
|
||||||
|
) as zoneminder_mock:
|
||||||
|
zm_client: ZoneMinder = MagicMock(spec=ZoneMinder)
|
||||||
|
zm_client.get_zms_url.return_value = "http://host1/path_zms1"
|
||||||
|
zm_client.login.return_value = True
|
||||||
|
zm_client.get_monitors.return_value = []
|
||||||
|
|
||||||
|
zoneminder_mock.return_value = zm_client
|
||||||
|
|
||||||
|
hass_config = {const.DOMAIN: []}
|
||||||
|
await async_setup_component(hass, const.DOMAIN, hass_config)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert not hass.services.has_service(const.DOMAIN, const.SERVICE_SET_RUN_STATE)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_yaml_config_import(hass: HomeAssistant) -> None:
|
||||||
|
"""Test yaml config import."""
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.zoneminder.common.ZoneMinder", autospec=ZoneMinder
|
||||||
|
) as zoneminder_mock:
|
||||||
|
zm_client: ZoneMinder = MagicMock(spec=ZoneMinder)
|
||||||
|
zm_client.get_zms_url.return_value = "http://host1/path_zms1"
|
||||||
|
zm_client.login.return_value = True
|
||||||
|
zm_client.get_monitors.return_value = []
|
||||||
|
|
||||||
|
zoneminder_mock.return_value = zm_client
|
||||||
|
|
||||||
|
hass_config = {const.DOMAIN: [{CONF_HOST: "host1"}]}
|
||||||
|
await async_setup_component(hass, const.DOMAIN, hass_config)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert hass.services.has_service(const.DOMAIN, const.SERVICE_SET_RUN_STATE)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_load_call_service_and_unload(hass: HomeAssistant) -> None:
|
||||||
|
"""Test config entry load/unload and calling of service."""
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.zoneminder.common.ZoneMinder", autospec=ZoneMinder
|
||||||
|
) as zoneminder_mock:
|
||||||
|
zm_client: ZoneMinder = MagicMock(spec=ZoneMinder)
|
||||||
|
zm_client.get_zms_url.return_value = "http://host1/path_zms1"
|
||||||
|
zm_client.login.side_effect = [True, True, False, True]
|
||||||
|
zm_client.get_monitors.return_value = []
|
||||||
|
zm_client.is_available.return_value = True
|
||||||
|
|
||||||
|
zoneminder_mock.return_value = zm_client
|
||||||
|
|
||||||
|
await hass.config_entries.flow.async_init(
|
||||||
|
const.DOMAIN,
|
||||||
|
context={CONF_SOURCE: config_entries.SOURCE_USER},
|
||||||
|
data={
|
||||||
|
CONF_HOST: "host1",
|
||||||
|
CONF_USERNAME: "username1",
|
||||||
|
CONF_PASSWORD: "password1",
|
||||||
|
CONF_PATH: "path1",
|
||||||
|
const.CONF_PATH_ZMS: "path_zms1",
|
||||||
|
CONF_SSL: False,
|
||||||
|
CONF_VERIFY_SSL: True,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
config_entry = next(iter(hass.config_entries.async_entries(const.DOMAIN)), None)
|
||||||
|
assert config_entry
|
||||||
|
|
||||||
|
assert config_entry.state == ENTRY_STATE_SETUP_RETRY
|
||||||
|
assert not is_client_in_data(hass, "host1")
|
||||||
|
|
||||||
|
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10))
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert config_entry.state == ENTRY_STATE_LOADED
|
||||||
|
assert is_client_in_data(hass, "host1")
|
||||||
|
|
||||||
|
assert hass.services.has_service(const.DOMAIN, const.SERVICE_SET_RUN_STATE)
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
const.DOMAIN,
|
||||||
|
const.SERVICE_SET_RUN_STATE,
|
||||||
|
{ATTR_ID: "host1", ATTR_NAME: "away"},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
zm_client.set_active_state.assert_called_with("away")
|
||||||
|
|
||||||
|
await config_entry.async_unload(hass)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert config_entry.state == ENTRY_STATE_NOT_LOADED
|
||||||
|
assert not is_client_in_data(hass, "host1")
|
||||||
|
assert not hass.services.has_service(const.DOMAIN, const.SERVICE_SET_RUN_STATE)
|
167
tests/components/zoneminder/test_sensor.py
Normal file
167
tests/components/zoneminder/test_sensor.py
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
"""Binary sensor tests."""
|
||||||
|
from zoneminder.monitor import Monitor, MonitorState, TimePeriod
|
||||||
|
from zoneminder.zm import ZoneMinder
|
||||||
|
|
||||||
|
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
|
||||||
|
from homeassistant.components.zoneminder import const
|
||||||
|
from homeassistant.components.zoneminder.sensor import CONF_INCLUDE_ARCHIVED
|
||||||
|
from homeassistant.config import async_process_ha_core_config
|
||||||
|
from homeassistant.const import (
|
||||||
|
ATTR_ENTITY_ID,
|
||||||
|
CONF_HOST,
|
||||||
|
CONF_MONITORED_CONDITIONS,
|
||||||
|
CONF_PASSWORD,
|
||||||
|
CONF_PATH,
|
||||||
|
CONF_PLATFORM,
|
||||||
|
CONF_SSL,
|
||||||
|
CONF_USERNAME,
|
||||||
|
CONF_VERIFY_SSL,
|
||||||
|
)
|
||||||
|
from homeassistant.core import DOMAIN as HASS_DOMAIN, HomeAssistant
|
||||||
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
|
from tests.async_mock import MagicMock, patch
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
|
||||||
|
async def test_async_setup_entry(hass: HomeAssistant) -> None:
|
||||||
|
"""Test setup of sensor entities."""
|
||||||
|
|
||||||
|
def _get_events(monitor_id: int, time_period: TimePeriod, include_archived: bool):
|
||||||
|
enum_list = [name for name in dir(TimePeriod) if not name.startswith("_")]
|
||||||
|
tp_index = enum_list.index(time_period.name)
|
||||||
|
return (100 * monitor_id) + (tp_index * 10) + include_archived
|
||||||
|
|
||||||
|
def _monitor1_get_events(time_period: TimePeriod, include_archived: bool):
|
||||||
|
return _get_events(1, time_period, include_archived)
|
||||||
|
|
||||||
|
def _monitor2_get_events(time_period: TimePeriod, include_archived: bool):
|
||||||
|
return _get_events(2, time_period, include_archived)
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.zoneminder.common.ZoneMinder", autospec=ZoneMinder
|
||||||
|
) as zoneminder_mock:
|
||||||
|
monitor1 = MagicMock(spec=Monitor)
|
||||||
|
monitor1.name = "monitor1"
|
||||||
|
monitor1.mjpeg_image_url = "mjpeg_image_url1"
|
||||||
|
monitor1.still_image_url = "still_image_url1"
|
||||||
|
monitor1.is_recording = True
|
||||||
|
monitor1.is_available = True
|
||||||
|
monitor1.function = MonitorState.MONITOR
|
||||||
|
monitor1.get_events.side_effect = _monitor1_get_events
|
||||||
|
|
||||||
|
monitor2 = MagicMock(spec=Monitor)
|
||||||
|
monitor2.name = "monitor2"
|
||||||
|
monitor2.mjpeg_image_url = "mjpeg_image_url2"
|
||||||
|
monitor2.still_image_url = "still_image_url2"
|
||||||
|
monitor2.is_recording = False
|
||||||
|
monitor2.is_available = False
|
||||||
|
monitor2.function = MonitorState.MODECT
|
||||||
|
monitor2.get_events.side_effect = _monitor2_get_events
|
||||||
|
|
||||||
|
zm_client: ZoneMinder = MagicMock(spec=ZoneMinder)
|
||||||
|
zm_client.get_zms_url.return_value = "http://host1/path_zms1"
|
||||||
|
zm_client.login.return_value = True
|
||||||
|
zm_client.get_monitors.return_value = [monitor1, monitor2]
|
||||||
|
|
||||||
|
zoneminder_mock.return_value = zm_client
|
||||||
|
|
||||||
|
config_entry = MockConfigEntry(
|
||||||
|
domain=const.DOMAIN,
|
||||||
|
unique_id="host1",
|
||||||
|
data={
|
||||||
|
CONF_HOST: "host1",
|
||||||
|
CONF_USERNAME: "username1",
|
||||||
|
CONF_PASSWORD: "password1",
|
||||||
|
CONF_PATH: "path1",
|
||||||
|
const.CONF_PATH_ZMS: "path_zms1",
|
||||||
|
CONF_SSL: False,
|
||||||
|
CONF_VERIFY_SSL: True,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
config_entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
hass_config = {
|
||||||
|
HASS_DOMAIN: {},
|
||||||
|
SENSOR_DOMAIN: [
|
||||||
|
{
|
||||||
|
CONF_PLATFORM: const.DOMAIN,
|
||||||
|
CONF_INCLUDE_ARCHIVED: True,
|
||||||
|
CONF_MONITORED_CONDITIONS: ["all", "day"],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
await async_process_ha_core_config(hass, hass_config[HASS_DOMAIN])
|
||||||
|
await async_setup_component(hass, HASS_DOMAIN, hass_config)
|
||||||
|
await async_setup_component(hass, SENSOR_DOMAIN, hass_config)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
await async_setup_component(hass, const.DOMAIN, hass_config)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
HASS_DOMAIN, "update_entity", {ATTR_ENTITY_ID: "sensor.monitor1_status"}
|
||||||
|
)
|
||||||
|
await hass.services.async_call(
|
||||||
|
HASS_DOMAIN, "update_entity", {ATTR_ENTITY_ID: "sensor.monitor1_events"}
|
||||||
|
)
|
||||||
|
await hass.services.async_call(
|
||||||
|
HASS_DOMAIN,
|
||||||
|
"update_entity",
|
||||||
|
{ATTR_ENTITY_ID: "sensor.monitor1_events_last_day"},
|
||||||
|
)
|
||||||
|
await hass.services.async_call(
|
||||||
|
HASS_DOMAIN, "update_entity", {ATTR_ENTITY_ID: "sensor.monitor2_status"}
|
||||||
|
)
|
||||||
|
await hass.services.async_call(
|
||||||
|
HASS_DOMAIN, "update_entity", {ATTR_ENTITY_ID: "sensor.monitor2_events"}
|
||||||
|
)
|
||||||
|
await hass.services.async_call(
|
||||||
|
HASS_DOMAIN,
|
||||||
|
"update_entity",
|
||||||
|
{ATTR_ENTITY_ID: "sensor.monitor2_events_last_day"},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert (
|
||||||
|
hass.states.get("sensor.monitor1_status").state
|
||||||
|
== MonitorState.MONITOR.value
|
||||||
|
)
|
||||||
|
assert hass.states.get("sensor.monitor1_events").state == "101"
|
||||||
|
assert hass.states.get("sensor.monitor1_events_last_day").state == "111"
|
||||||
|
assert hass.states.get("sensor.monitor2_status").state == "unavailable"
|
||||||
|
assert hass.states.get("sensor.monitor2_events").state == "201"
|
||||||
|
assert hass.states.get("sensor.monitor2_events_last_day").state == "211"
|
||||||
|
|
||||||
|
monitor1.function = MonitorState.NONE
|
||||||
|
monitor2.function = MonitorState.NODECT
|
||||||
|
await hass.services.async_call(
|
||||||
|
HASS_DOMAIN, "update_entity", {ATTR_ENTITY_ID: "sensor.monitor1_status"}
|
||||||
|
)
|
||||||
|
await hass.services.async_call(
|
||||||
|
HASS_DOMAIN, "update_entity", {ATTR_ENTITY_ID: "sensor.monitor1_events"}
|
||||||
|
)
|
||||||
|
await hass.services.async_call(
|
||||||
|
HASS_DOMAIN,
|
||||||
|
"update_entity",
|
||||||
|
{ATTR_ENTITY_ID: "sensor.monitor1_events_last_day"},
|
||||||
|
)
|
||||||
|
await hass.services.async_call(
|
||||||
|
HASS_DOMAIN, "update_entity", {ATTR_ENTITY_ID: "sensor.monitor2_status"}
|
||||||
|
)
|
||||||
|
await hass.services.async_call(
|
||||||
|
HASS_DOMAIN, "update_entity", {ATTR_ENTITY_ID: "sensor.monitor2_events"}
|
||||||
|
)
|
||||||
|
await hass.services.async_call(
|
||||||
|
HASS_DOMAIN,
|
||||||
|
"update_entity",
|
||||||
|
{ATTR_ENTITY_ID: "sensor.monitor2_events_last_day"},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert (
|
||||||
|
hass.states.get("sensor.monitor1_status").state == MonitorState.NONE.value
|
||||||
|
)
|
||||||
|
assert hass.states.get("sensor.monitor1_events").state == "101"
|
||||||
|
assert hass.states.get("sensor.monitor1_events_last_day").state == "111"
|
||||||
|
assert hass.states.get("sensor.monitor2_status").state == "unavailable"
|
||||||
|
assert hass.states.get("sensor.monitor2_events").state == "201"
|
||||||
|
assert hass.states.get("sensor.monitor2_events_last_day").state == "211"
|
126
tests/components/zoneminder/test_switch.py
Normal file
126
tests/components/zoneminder/test_switch.py
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
"""Binary sensor tests."""
|
||||||
|
from zoneminder.monitor import Monitor, MonitorState
|
||||||
|
from zoneminder.zm import ZoneMinder
|
||||||
|
|
||||||
|
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
|
||||||
|
from homeassistant.components.zoneminder import const
|
||||||
|
from homeassistant.config import async_process_ha_core_config
|
||||||
|
from homeassistant.const import (
|
||||||
|
ATTR_ENTITY_ID,
|
||||||
|
CONF_COMMAND_OFF,
|
||||||
|
CONF_COMMAND_ON,
|
||||||
|
CONF_HOST,
|
||||||
|
CONF_PASSWORD,
|
||||||
|
CONF_PATH,
|
||||||
|
CONF_PLATFORM,
|
||||||
|
CONF_SSL,
|
||||||
|
CONF_USERNAME,
|
||||||
|
CONF_VERIFY_SSL,
|
||||||
|
STATE_OFF,
|
||||||
|
STATE_ON,
|
||||||
|
)
|
||||||
|
from homeassistant.core import DOMAIN as HASS_DOMAIN, HomeAssistant
|
||||||
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
|
from tests.async_mock import MagicMock, patch
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
|
||||||
|
async def test_async_setup_entry(hass: HomeAssistant) -> None:
|
||||||
|
"""Test setup of sensor entities."""
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.zoneminder.common.ZoneMinder", autospec=ZoneMinder
|
||||||
|
) as zoneminder_mock:
|
||||||
|
monitor1 = MagicMock(spec=Monitor)
|
||||||
|
monitor1.name = "monitor1"
|
||||||
|
monitor1.mjpeg_image_url = "mjpeg_image_url1"
|
||||||
|
monitor1.still_image_url = "still_image_url1"
|
||||||
|
monitor1.is_recording = True
|
||||||
|
monitor1.is_available = True
|
||||||
|
monitor1.function = MonitorState.MONITOR
|
||||||
|
|
||||||
|
monitor2 = MagicMock(spec=Monitor)
|
||||||
|
monitor2.name = "monitor2"
|
||||||
|
monitor2.mjpeg_image_url = "mjpeg_image_url2"
|
||||||
|
monitor2.still_image_url = "still_image_url2"
|
||||||
|
monitor2.is_recording = False
|
||||||
|
monitor2.is_available = False
|
||||||
|
monitor2.function = MonitorState.MODECT
|
||||||
|
|
||||||
|
zm_client: ZoneMinder = MagicMock(spec=ZoneMinder)
|
||||||
|
zm_client.get_zms_url.return_value = "http://host1/path_zms1"
|
||||||
|
zm_client.login.return_value = True
|
||||||
|
zm_client.get_monitors.return_value = [monitor1, monitor2]
|
||||||
|
|
||||||
|
zoneminder_mock.return_value = zm_client
|
||||||
|
|
||||||
|
config_entry = MockConfigEntry(
|
||||||
|
domain=const.DOMAIN,
|
||||||
|
unique_id="host1",
|
||||||
|
data={
|
||||||
|
CONF_HOST: "host1",
|
||||||
|
CONF_USERNAME: "username1",
|
||||||
|
CONF_PASSWORD: "password1",
|
||||||
|
CONF_PATH: "path1",
|
||||||
|
const.CONF_PATH_ZMS: "path_zms1",
|
||||||
|
CONF_SSL: False,
|
||||||
|
CONF_VERIFY_SSL: True,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
config_entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
hass_config = {
|
||||||
|
HASS_DOMAIN: {},
|
||||||
|
SWITCH_DOMAIN: [
|
||||||
|
{
|
||||||
|
CONF_PLATFORM: const.DOMAIN,
|
||||||
|
CONF_COMMAND_ON: MonitorState.MONITOR.value,
|
||||||
|
CONF_COMMAND_OFF: MonitorState.MODECT.value,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
CONF_PLATFORM: const.DOMAIN,
|
||||||
|
CONF_COMMAND_ON: MonitorState.MODECT.value,
|
||||||
|
CONF_COMMAND_OFF: MonitorState.MONITOR.value,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
await async_process_ha_core_config(hass, hass_config[HASS_DOMAIN])
|
||||||
|
await async_setup_component(hass, HASS_DOMAIN, hass_config)
|
||||||
|
await async_setup_component(hass, SWITCH_DOMAIN, hass_config)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
await async_setup_component(hass, const.DOMAIN, hass_config)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
SWITCH_DOMAIN, "turn_on", {ATTR_ENTITY_ID: "switch.monitor1_state"}
|
||||||
|
)
|
||||||
|
await hass.services.async_call(
|
||||||
|
SWITCH_DOMAIN, "turn_off", {ATTR_ENTITY_ID: "switch.monitor1_state_2"}
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert hass.states.get("switch.monitor1_state").state == STATE_ON
|
||||||
|
assert hass.states.get("switch.monitor1_state_2").state == STATE_OFF
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
SWITCH_DOMAIN, "turn_off", {ATTR_ENTITY_ID: "switch.monitor1_state"}
|
||||||
|
)
|
||||||
|
await hass.services.async_call(
|
||||||
|
SWITCH_DOMAIN, "turn_on", {ATTR_ENTITY_ID: "switch.monitor1_state_2"}
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert hass.states.get("switch.monitor1_state").state == STATE_OFF
|
||||||
|
assert hass.states.get("switch.monitor1_state_2").state == STATE_ON
|
||||||
|
|
||||||
|
monitor1.function = MonitorState.NONE
|
||||||
|
monitor2.function = MonitorState.NODECT
|
||||||
|
await hass.services.async_call(
|
||||||
|
HASS_DOMAIN, "update_entity", {ATTR_ENTITY_ID: "switch.monitor1_state"}
|
||||||
|
)
|
||||||
|
await hass.services.async_call(
|
||||||
|
HASS_DOMAIN, "update_entity", {ATTR_ENTITY_ID: "switch.monitor1_state_2"}
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert hass.states.get("switch.monitor1_state").state == STATE_OFF
|
||||||
|
assert hass.states.get("switch.monitor1_state_2").state == STATE_OFF
|
Loading…
x
Reference in New Issue
Block a user