From 1cbf8c804935029ad69497510f8e1e81a71fd889 Mon Sep 17 00:00:00 2001 From: Richard Cox Date: Fri, 14 Oct 2016 20:56:40 -0700 Subject: [PATCH] Zoneminder component (#3795) * Initial Zoneminder commit * Fixing bug when ZM sets its function to 'None' * Adding zoneminder to coverage * Quick Doc fix * Update zoneminder.py Doc Fix * making the url base optional --- .coveragerc | 3 + homeassistant/components/sensor/zoneminder.py | 93 ++++++++++++++ homeassistant/components/switch/zoneminder.py | 92 ++++++++++++++ homeassistant/components/zoneminder.py | 119 ++++++++++++++++++ 4 files changed, 307 insertions(+) create mode 100644 homeassistant/components/sensor/zoneminder.py create mode 100644 homeassistant/components/switch/zoneminder.py create mode 100644 homeassistant/components/zoneminder.py diff --git a/.coveragerc b/.coveragerc index a5fcdf7f9c1..340eccd22a4 100644 --- a/.coveragerc +++ b/.coveragerc @@ -104,6 +104,9 @@ omit = homeassistant/components/ffmpeg.py homeassistant/components/*/ffmpeg.py + homeassistant/components/zoneminder.py + homeassistant/components/*/zoneminder.py + homeassistant/components/alarm_control_panel/alarmdotcom.py homeassistant/components/alarm_control_panel/nx584.py homeassistant/components/alarm_control_panel/simplisafe.py diff --git a/homeassistant/components/sensor/zoneminder.py b/homeassistant/components/sensor/zoneminder.py new file mode 100644 index 00000000000..50446f735c3 --- /dev/null +++ b/homeassistant/components/sensor/zoneminder.py @@ -0,0 +1,93 @@ +""" +Support for Zoneminder Sensors. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.zoneminder/ +""" +import logging + +import homeassistant.components.zoneminder as zoneminder +from homeassistant.helpers.entity import Entity + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['zoneminder'] + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup Zoneminder platform.""" + sensors = [] + + monitors = zoneminder.get_state('api/monitors.json') + for i in monitors['monitors']: + sensors.append( + ZMSensorMonitors(int(i['Monitor']['Id']), i['Monitor']['Name']) + ) + sensors.append( + ZMSensorEvents(int(i['Monitor']['Id']), i['Monitor']['Name']) + ) + + add_devices(sensors) + + +class ZMSensorMonitors(Entity): + """Get the status of each monitor.""" + + def __init__(self, monitor_id, monitor_name): + """Initiate monitor sensor.""" + self._monitor_id = monitor_id + self._monitor_name = monitor_name + self._state = None + + @property + def name(self): + """Return the name of the sensor.""" + return "%s Status" % self._monitor_name + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + def update(self): + """Update the sensor.""" + monitor = zoneminder.get_state( + 'api/monitors/%i.json' % self._monitor_id + ) + if monitor['monitor']['Monitor']['Function'] is None: + self._state = "None" + else: + self._state = monitor['monitor']['Monitor']['Function'] + + +class ZMSensorEvents(Entity): + """Get the number of events for each monitor.""" + + def __init__(self, monitor_id, monitor_name): + """Initiate event sensor.""" + self._monitor_id = monitor_id + self._monitor_name = monitor_name + self._state = None + + @property + def name(self): + """Return the name of the sensor.""" + return "%s Events" % self._monitor_name + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this entity, if any.""" + return 'Events' + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + def update(self): + """Update the sensor.""" + event = zoneminder.get_state( + 'api/events/index/MonitorId:%i.json' % self._monitor_id + ) + + self._state = event['pagination']['count'] diff --git a/homeassistant/components/switch/zoneminder.py b/homeassistant/components/switch/zoneminder.py new file mode 100644 index 00000000000..ab9adbca97d --- /dev/null +++ b/homeassistant/components/switch/zoneminder.py @@ -0,0 +1,92 @@ +""" +Support for Zoneminder switches. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/switch.zoneminder/ +""" +import logging + +import voluptuous as vol + +from homeassistant.components.switch import (SwitchDevice, PLATFORM_SCHEMA) +from homeassistant.const import (CONF_COMMAND_ON, CONF_COMMAND_OFF) +import homeassistant.helpers.config_validation as cv + +import homeassistant.components.zoneminder as zoneminder + + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_COMMAND_ON): cv.string, + vol.Required(CONF_COMMAND_OFF): cv.string, +}) + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['zoneminder'] + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the Zoneminder switch.""" + on_state = config.get(CONF_COMMAND_ON) + off_state = config.get(CONF_COMMAND_OFF) + + switches = [] + + monitors = zoneminder.get_state('api/monitors.json') + for i in monitors['monitors']: + switches.append( + ZMSwitchMonitors( + int(i['Monitor']['Id']), + i['Monitor']['Name'], + on_state, + off_state + ) + ) + + add_devices(switches) + + +class ZMSwitchMonitors(SwitchDevice): + """Representation of an zoneminder switch.""" + + icon = 'mdi:record-rec' + + def __init__(self, monitor_id, monitor_name, on_state, off_state): + """Initialize the switch.""" + self._monitor_id = monitor_id + self._monitor_name = monitor_name + self._on_state = on_state + self._off_state = off_state + self._state = None + + @property + def name(self): + """Return the name of the switch.""" + return "%s State" % self._monitor_name + + def update(self): + """Update the switch value.""" + monitor = zoneminder.get_state( + 'api/monitors/%i.json' % self._monitor_id + ) + current_state = monitor['monitor']['Monitor']['Function'] + self._state = True if current_state == self._on_state else False + + @property + def is_on(self): + """Return True if entity is on.""" + return self._state + + def turn_on(self): + """Turn the entity on.""" + zoneminder.change_state( + 'api/monitors/%i.json' % self._monitor_id, + {'Monitor[Function]': self._on_state} + ) + + def turn_off(self): + """Turn the entity off.""" + zoneminder.change_state( + 'api/monitors/%i.json' % self._monitor_id, + {'Monitor[Function]': self._off_state} + ) diff --git a/homeassistant/components/zoneminder.py b/homeassistant/components/zoneminder.py new file mode 100644 index 00000000000..3f43ae01904 --- /dev/null +++ b/homeassistant/components/zoneminder.py @@ -0,0 +1,119 @@ +""" +Support for Zoneminder. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/zoneminder/ +""" + +import logging +import json +from urllib.parse import urljoin + +import requests +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.const import ( + CONF_URL, CONF_HOST, CONF_PASSWORD, CONF_USERNAME) + + +_LOGGER = logging.getLogger(__name__) + +REQUIREMENTS = [] + +DOMAIN = 'zoneminder' + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_URL, default="/zm/"): cv.string, + vol.Optional(CONF_USERNAME): cv.string, + vol.Optional(CONF_PASSWORD): cv.string + }) +}, extra=vol.ALLOW_EXTRA) + +LOGIN_RETRIES = 2 +ZM = {} + + +def setup(hass, config): + """Setup the zonminder platform.""" + global ZM + ZM = {} + + conf = config[DOMAIN] + url = urljoin("http://" + conf[CONF_HOST], conf[CONF_URL]) + username = conf.get(CONF_USERNAME, None) + password = conf.get(CONF_PASSWORD, None) + + ZM['url'] = url + ZM['username'] = username + ZM['password'] = password + + return login() + + +# pylint: disable=no-member +def login(): + """Login to the zoneminder api.""" + _LOGGER.debug("Attempting to login to zoneminder") + + login_post = {'view': 'console', 'action': 'login'} + if ZM['username']: + login_post['username'] = ZM['username'] + if ZM['password']: + login_post['password'] = ZM['password'] + + req = requests.post(ZM['url'] + '/index.php', data=login_post) + ZM['cookies'] = req.cookies + + # Login calls returns a 200 repsonse on both failure and success.. + # The only way to tell if you logged in correctly is to issue an api call. + req = requests.get( + ZM['url'] + 'api/host/getVersion.json', + cookies=ZM['cookies'] + ) + + if req.status_code != requests.codes.ok: + _LOGGER.error("Connection error logging into ZoneMinder") + return False + + return True + + +# pylint: disable=no-member +def get_state(api_url): + """Get a state from the zoneminder API service.""" + # Since the API uses sessions that expire, sometimes we need + # to re-auth if the call fails. + for _ in range(LOGIN_RETRIES): + req = requests.get(urljoin(ZM['url'], api_url), cookies=ZM['cookies']) + + if req.status_code != requests.codes.ok: + login() + else: + break + else: + _LOGGER.exception("Unable to get API response") + + return json.loads(req.text) + + +# pylint: disable=no-member +def change_state(api_url, post_data): + """Update a state using the Zoneminder API.""" + for _ in range(LOGIN_RETRIES): + req = requests.post( + urljoin(ZM['url'], api_url), + data=post_data, + cookies=ZM['cookies']) + + if req.status_code != requests.codes.ok: + login() + else: + break + + else: + _LOGGER.exception("Unable to get API response") + + return json.loads(req.text)