295 lines
		
	
	
		
			8.4 KiB
		
	
	
	
		
			Python
		
	
	
			
		
		
	
	
			295 lines
		
	
	
		
			8.4 KiB
		
	
	
	
		
			Python
		
	
	
"""Support for Hikvision event stream events represented as binary sensors."""
 | 
						|
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,
 | 
						|
    EVENT_HOMEASSISTANT_START,
 | 
						|
    ATTR_LAST_TRIP_TIME,
 | 
						|
    CONF_CUSTOMIZE,
 | 
						|
)
 | 
						|
 | 
						|
_LOGGER = logging.getLogger(__name__)
 | 
						|
 | 
						|
CONF_IGNORED = "ignored"
 | 
						|
CONF_DELAY = "delay"
 | 
						|
 | 
						|
DEFAULT_PORT = 80
 | 
						|
DEFAULT_IGNORED = False
 | 
						|
DEFAULT_DELAY = 0
 | 
						|
 | 
						|
ATTR_DELAY = "delay"
 | 
						|
 | 
						|
DEVICE_CLASS_MAP = {
 | 
						|
    "Motion": "motion",
 | 
						|
    "Line Crossing": "motion",
 | 
						|
    "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",
 | 
						|
    "Scene Change Detection": "motion",
 | 
						|
    "I/O": None,
 | 
						|
    "Unattended Baggage": "motion",
 | 
						|
    "Attended Baggage": "motion",
 | 
						|
    "Recording Failure": None,
 | 
						|
    "Exiting Region": "motion",
 | 
						|
    "Entering Region": "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): 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):
 | 
						|
    """Set up the 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 = f"{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 set up")
 | 
						|
        return False
 | 
						|
 | 
						|
    entities = []
 | 
						|
 | 
						|
    for sensor, channel_list in data.sensors.items():
 | 
						|
        for channel in channel_list:
 | 
						|
            # Build sensor name, then parse customize config.
 | 
						|
            if data.type == "NVR":
 | 
						|
                sensor_name = "{}_{}".format(sensor.replace(" ", "_"), channel[1])
 | 
						|
            else:
 | 
						|
                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, channel[1], data, delay)
 | 
						|
                )
 | 
						|
 | 
						|
    add_entities(entities)
 | 
						|
 | 
						|
 | 
						|
class HikvisionData:
 | 
						|
    """Hikvision device event stream object."""
 | 
						|
 | 
						|
    def __init__(self, hass, url, port, name, username, password):
 | 
						|
        """Initialize the data object."""
 | 
						|
        from pyhik.hikvision import HikCamera
 | 
						|
 | 
						|
        self._url = url
 | 
						|
        self._port = port
 | 
						|
        self._name = name
 | 
						|
        self._username = username
 | 
						|
        self._password = password
 | 
						|
 | 
						|
        # Establish camera
 | 
						|
        self.camdata = HikCamera(self._url, self._port, self._username, self._password)
 | 
						|
 | 
						|
        if self._name is None:
 | 
						|
            self._name = self.camdata.get_name
 | 
						|
 | 
						|
        hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, self.stop_hik)
 | 
						|
        hass.bus.listen_once(EVENT_HOMEASSISTANT_START, self.start_hik)
 | 
						|
 | 
						|
    def stop_hik(self, event):
 | 
						|
        """Shutdown Hikvision subscriptions and subscription thread on exit."""
 | 
						|
        self.camdata.disconnect()
 | 
						|
 | 
						|
    def start_hik(self, event):
 | 
						|
        """Start Hikvision event stream thread."""
 | 
						|
        self.camdata.start_stream()
 | 
						|
 | 
						|
    @property
 | 
						|
    def sensors(self):
 | 
						|
        """Return list of available sensors and their states."""
 | 
						|
        return self.camdata.current_event_states
 | 
						|
 | 
						|
    @property
 | 
						|
    def cam_id(self):
 | 
						|
        """Return device id."""
 | 
						|
        return self.camdata.get_id
 | 
						|
 | 
						|
    @property
 | 
						|
    def name(self):
 | 
						|
        """Return device name."""
 | 
						|
        return self._name
 | 
						|
 | 
						|
    @property
 | 
						|
    def type(self):
 | 
						|
        """Return device type."""
 | 
						|
        return self.camdata.get_type
 | 
						|
 | 
						|
    def get_attributes(self, sensor, channel):
 | 
						|
        """Return attribute list for sensor/channel."""
 | 
						|
        return self.camdata.fetch_attributes(sensor, channel)
 | 
						|
 | 
						|
 | 
						|
class HikvisionBinarySensor(BinarySensorDevice):
 | 
						|
    """Representation of a Hikvision binary sensor."""
 | 
						|
 | 
						|
    def __init__(self, hass, sensor, channel, cam, delay):
 | 
						|
        """Initialize the binary_sensor."""
 | 
						|
        self._hass = hass
 | 
						|
        self._cam = cam
 | 
						|
        self._sensor = sensor
 | 
						|
        self._channel = channel
 | 
						|
 | 
						|
        if self._cam.type == "NVR":
 | 
						|
            self._name = f"{self._cam.name} {sensor} {channel}"
 | 
						|
        else:
 | 
						|
            self._name = f"{self._cam.name} {sensor}"
 | 
						|
 | 
						|
        self._id = f"{self._cam.cam_id}.{sensor}.{channel}"
 | 
						|
 | 
						|
        if delay is None:
 | 
						|
            self._delay = 0
 | 
						|
        else:
 | 
						|
            self._delay = delay
 | 
						|
 | 
						|
        self._timer = None
 | 
						|
 | 
						|
        # Register callback function with pyHik
 | 
						|
        self._cam.camdata.add_update_callback(self._update_callback, self._id)
 | 
						|
 | 
						|
    def _sensor_state(self):
 | 
						|
        """Extract sensor state."""
 | 
						|
        return self._cam.get_attributes(self._sensor, self._channel)[0]
 | 
						|
 | 
						|
    def _sensor_last_update(self):
 | 
						|
        """Extract sensor last update time."""
 | 
						|
        return self._cam.get_attributes(self._sensor, self._channel)[3]
 | 
						|
 | 
						|
    @property
 | 
						|
    def name(self):
 | 
						|
        """Return the name of the Hikvision sensor."""
 | 
						|
        return self._name
 | 
						|
 | 
						|
    @property
 | 
						|
    def unique_id(self):
 | 
						|
        """Return a unique ID."""
 | 
						|
        return self._id
 | 
						|
 | 
						|
    @property
 | 
						|
    def is_on(self):
 | 
						|
        """Return true if sensor is on."""
 | 
						|
        return self._sensor_state()
 | 
						|
 | 
						|
    @property
 | 
						|
    def device_class(self):
 | 
						|
        """Return the class of this sensor, from DEVICE_CLASSES."""
 | 
						|
        try:
 | 
						|
            return DEVICE_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, msg):
 | 
						|
        """Update the sensor's state, if needed."""
 | 
						|
        _LOGGER.debug("Callback signal from: %s", msg)
 | 
						|
 | 
						|
        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()
 |