From acb841a1f4bf2507bbec4826ec5516e9c8c47d9c Mon Sep 17 00:00:00 2001 From: John Mihalic <mezz@johnmihalic.com> Date: Tue, 13 Dec 2016 02:10:16 -0500 Subject: [PATCH] Add Hikvision binary sensor component (#4825) * Add Hikvision binary sensor component * Simplify customize configuration * Add delay attribute * Remove use of threading timer, fix delay functionality --- .coveragerc | 1 + .../components/binary_sensor/hikvision.py | 262 ++++++++++++++++++ requirements_all.txt | 4 + 3 files changed, 267 insertions(+) create mode 100644 homeassistant/components/binary_sensor/hikvision.py diff --git a/.coveragerc b/.coveragerc index 4aae4cbc242..3168a20bd8c 100644 --- a/.coveragerc +++ b/.coveragerc @@ -123,6 +123,7 @@ omit = homeassistant/components/binary_sensor/arest.py homeassistant/components/binary_sensor/concord232.py homeassistant/components/binary_sensor/flic.py + homeassistant/components/binary_sensor/hikvision.py homeassistant/components/binary_sensor/rest.py homeassistant/components/browser.py homeassistant/components/camera/amcrest.py diff --git a/homeassistant/components/binary_sensor/hikvision.py b/homeassistant/components/binary_sensor/hikvision.py new file mode 100644 index 00000000000..90d61cbf3b7 --- /dev/null +++ b/homeassistant/components/binary_sensor/hikvision.py @@ -0,0 +1,262 @@ +""" +Support for Hikvision event stream events represented as binary sensors. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/binary_sensor.hikvision/ +""" +import logging +from datetime import timedelta +import voluptuous as vol + +from homeassistant.helpers.event import track_point_in_utc_time +from homeassistant.util.dt import utcnow +from homeassistant.components.binary_sensor import ( + BinarySensorDevice, PLATFORM_SCHEMA) +import homeassistant.helpers.config_validation as cv +from homeassistant.const import ( + CONF_HOST, CONF_PORT, CONF_NAME, CONF_USERNAME, CONF_PASSWORD, + CONF_SSL, EVENT_HOMEASSISTANT_STOP, ATTR_LAST_TRIP_TIME, CONF_CUSTOMIZE) + +REQUIREMENTS = ['pyhik==0.0.6', 'pydispatcher==2.0.5'] +_LOGGER = logging.getLogger(__name__) + +CONF_IGNORED = 'ignored' +CONF_DELAY = 'delay' + +DEFAULT_PORT = 80 +DEFAULT_IGNORED = False +DEFAULT_DELAY = 0 + +ATTR_DELAY = 'delay' + +SENSOR_CLASS_MAP = { + 'Motion': 'motion', + 'Line Crossing': 'motion', + 'IO Trigger': None, + 'Field Detection': 'motion', + 'Video Loss': None, + 'Tamper Detection': 'motion', + 'Shelter Alarm': None, + 'Disk Full': None, + 'Disk Error': None, + 'Net Interface Broken': 'connectivity', + 'IP Conflict': 'connectivity', + 'Illegal Access': None, + 'Video Mismatch': None, + 'Bad Video': None, + 'PIR Alarm': 'motion', + 'Face Detection': 'motion', +} + +CUSTOMIZE_SCHEMA = vol.Schema({ + vol.Optional(CONF_IGNORED, default=DEFAULT_IGNORED): cv.boolean, + vol.Optional(CONF_DELAY, default=DEFAULT_DELAY): cv.positive_int + }) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_NAME, default=None): cv.string, + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_SSL, default=False): cv.boolean, + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_CUSTOMIZE, default={}): + vol.Schema({cv.string: CUSTOMIZE_SCHEMA}), +}) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Setup Hikvision binary sensor devices.""" + name = config.get(CONF_NAME) + host = config.get(CONF_HOST) + port = config.get(CONF_PORT) + username = config.get(CONF_USERNAME) + password = config.get(CONF_PASSWORD) + + customize = config.get(CONF_CUSTOMIZE) + + if config.get(CONF_SSL): + protocol = "https" + else: + protocol = "http" + + url = '{}://{}'.format(protocol, host) + + data = HikvisionData(hass, url, port, name, username, password) + + if data.sensors is None: + _LOGGER.error('Hikvision event stream has no data, unable to setup.') + return False + + entities = [] + + for sensor in data.sensors: + # Build sensor name, then parse customize config. + sensor_name = sensor.replace(' ', '_') + + custom = customize.get(sensor_name.lower(), {}) + ignore = custom.get(CONF_IGNORED) + delay = custom.get(CONF_DELAY) + + _LOGGER.debug('Entity: %s - %s, Options - Ignore: %s, Delay: %s', + data.name, sensor_name, ignore, delay) + if not ignore: + entities.append(HikvisionBinarySensor(hass, sensor, data, delay)) + + add_entities(entities) + + +class HikvisionData(object): + """Hikvision camera event stream object.""" + + def __init__(self, hass, url, port, name, username, password): + """Initialize the data oject.""" + from pyhik.hikvision import HikCamera + self._url = url + self._port = port + self._name = name + self._username = username + self._password = password + + # Establish camera + self._cam = HikCamera(self._url, self._port, + self._username, self._password) + + if self._name is None: + self._name = self._cam.get_name + + # Start event stream + self._cam.start_stream() + + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, self.stop_hik) + + def stop_hik(self, event): + """Shutdown Hikvision subscriptions and subscription thread on exit.""" + self._cam.disconnect() + + @property + def sensors(self): + """Return list of available sensors and their states.""" + return self._cam.current_event_states + + @property + def cam_id(self): + """Return camera id.""" + return self._cam.get_id + + @property + def name(self): + """Return camera name.""" + return self._name + + +class HikvisionBinarySensor(BinarySensorDevice): + """Representation of a Hikvision binary sensor.""" + + def __init__(self, hass, sensor, cam, delay): + """Initialize the binary_sensor.""" + from pydispatch import dispatcher + + self._hass = hass + self._cam = cam + self._name = self._cam.name + ' ' + sensor + self._id = self._cam.cam_id + '.' + sensor + self._sensor = sensor + + if delay is None: + self._delay = 0 + else: + self._delay = delay + + self._timer = None + + # Form signal for dispatcher + signal = 'ValueChanged.{}'.format(self._cam.cam_id) + + dispatcher.connect(self._update_callback, + signal=signal, + sender=self._sensor) + + def _sensor_state(self): + """Extract sensor state.""" + return self._cam.sensors[self._sensor][0] + + def _sensor_last_update(self): + """Extract sensor last update time.""" + return self._cam.sensors[self._sensor][3] + + @property + def name(self): + """Return the name of the Hikvision sensor.""" + return self._name + + @property + def unique_id(self): + """Return an unique ID.""" + return '{}.{}'.format(self.__class__, self._id) + + @property + def is_on(self): + """Return true if sensor is on.""" + return self._sensor_state() + + @property + def sensor_class(self): + """Return the class of this sensor, from SENSOR_CLASSES.""" + try: + return SENSOR_CLASS_MAP[self._sensor] + except KeyError: + # Sensor must be unknown to us, add as generic + return None + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def device_state_attributes(self): + """Return the state attributes.""" + attr = {} + attr[ATTR_LAST_TRIP_TIME] = self._sensor_last_update() + + if self._delay != 0: + attr[ATTR_DELAY] = self._delay + + return attr + + def _update_callback(self, signal, sender): + """Update the sensor's state, if needed.""" + _LOGGER.debug('Dispatcher callback, signal: %s, sender: %s', + signal, sender) + + if sender is not self._sensor: + return + + if self._delay > 0 and not self.is_on: + # Set timer to wait until updating the state + def _delay_update(now): + """Timer callback for sensor update.""" + _LOGGER.debug('%s Called delayed (%ssec) update.', + self._name, self._delay) + self.schedule_update_ha_state() + self._timer = None + + if self._timer is not None: + self._timer() + self._timer = None + + self._timer = track_point_in_utc_time( + self._hass, _delay_update, + utcnow() + timedelta(seconds=self._delay)) + + elif self._delay > 0 and self.is_on: + # For delayed sensors kill any callbacks on true events and update + if self._timer is not None: + self._timer() + self._timer = None + + self.schedule_update_ha_state() + + else: + self.schedule_update_ha_state() diff --git a/requirements_all.txt b/requirements_all.txt index 3e95167c1ee..799b2b29b11 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -387,6 +387,7 @@ pycmus==0.1.0 # homeassistant.components.envisalink # homeassistant.components.zwave +# homeassistant.components.binary_sensor.hikvision pydispatcher==2.0.5 # homeassistant.components.media_player.emby @@ -401,6 +402,9 @@ pyfttt==0.3 # homeassistant.components.remote.harmony pyharmony==1.0.12 +# homeassistant.components.binary_sensor.hikvision +pyhik==0.0.6 + # homeassistant.components.homematic pyhomematic==0.1.18 -- GitLab