Ejemplo n.º 1
0
    def __init__(self, host=None, port=DEFAULT_PORT, usr=None, pwd=None):
        """Initialize device."""

        _LOGGING.debug("pyHik %s initializing new hikvision device at: %s",
                       __version__, host)

        self.event_states = {}

        self.watchdog = Watchdog(300.0, self.watchdog_handler)

        if not host:
            _LOGGING.error('Host not specified! Cannot continue.')
            return

        self.host = host
        self.usr = usr
        self.pwd = pwd
        self.cam_id = 0
        self.name = ''
        self.device_type = None
        self.motion_detection = None
        self._motion_detection_xml = None

        self.root_url = '{}:{}'.format(host, port)

        self.namespace = {
            CONTEXT_INFO: None,
            CONTEXT_TRIG: None,
            CONTEXT_ALERT: None,
            CONTEXT_MOTION: None
        }

        # Build requests session for main thread calls
        # Default to basic authentication. It will change to digest inside
        # get_device_info if basic fails
        self.hik_request = requests.Session()
        self.hik_request.auth = (usr, pwd)
        self.hik_request.headers.update(DEFAULT_HEADERS)

        # Define event stream processing thread
        self.kill_thrd = threading.Event()
        self.reset_thrd = threading.Event()
        self.thrd = threading.Thread(target=self.alert_stream,
                                     args=(
                                         self.reset_thrd,
                                         self.kill_thrd,
                                     ))
        self.thrd.daemon = False

        # Callbacks
        self._updateCallbacks = []

        self.initialize()
Ejemplo n.º 2
0
    def __init__(self, host=None, port=DEFAULT_PORT, usr=None, pwd=None):
        """Initialize device."""

        _LOGGING.debug("pyHik %s initializing new hikvision device at: %s",
                       __version__, host)

        self.event_states = {}

        self.watchdog = Watchdog(300.0, self.watchdog_handler)

        self.namespace = XML_NAMESPACE

        if not host:
            _LOGGING.error('Host not specified! Cannot continue.')
            return

        self.host = host
        self.usr = usr
        self.pwd = pwd
        self.cam_id = 0
        self.name = ''
        self.device_type = None

        self.root_url = '{}:{}'.format(host, port)

        # Build requests session for main thread calls
        self.hik_request = requests.Session()
        self.hik_request.auth = (usr, pwd)
        self.hik_request.timeout = 5
        self.hik_request.headers.update(DEFAULT_HEADERS)

        # Define event stream processing thread
        self.kill_thrd = threading.Event()
        self.reset_thrd = threading.Event()
        self.thrd = threading.Thread(target=self.alert_stream,
                                     args=(
                                         self.reset_thrd,
                                         self.kill_thrd,
                                     ))
        self.thrd.daemon = False

        # Callbacks
        self._updateCallbacks = []

        self.initialize()
Ejemplo n.º 3
0
class HikCamera(object):
    """Creates a new Hikvision api device."""
    def __init__(self, host=None, port=DEFAULT_PORT, usr=None, pwd=None):
        """Initialize device."""

        _LOGGING.debug("pyHik %s initializing new hikvision device at: %s",
                       __version__, host)

        self.event_states = {}

        self.watchdog = Watchdog(300.0, self.watchdog_handler)

        self.namespace = XML_NAMESPACE

        if not host:
            _LOGGING.error('Host not specified! Cannot continue.')
            return

        self.host = host
        self.usr = usr
        self.pwd = pwd
        self.cam_id = 0
        self.name = ''
        self.device_type = None
        self.motion_detection = None
        self._motion_detection_xml = None

        self.root_url = '{}:{}'.format(host, port)

        # Build requests session for main thread calls
        # Default to basic authentication. It will change to digest inside
        # get_device_info if basic fails
        self.hik_request = requests.Session()
        self.hik_request.auth = (usr, pwd)
        self.hik_request.headers.update(DEFAULT_HEADERS)

        # Define event stream processing thread
        self.kill_thrd = threading.Event()
        self.reset_thrd = threading.Event()
        self.thrd = threading.Thread(target=self.alert_stream,
                                     args=(
                                         self.reset_thrd,
                                         self.kill_thrd,
                                     ))
        self.thrd.daemon = False

        # Callbacks
        self._updateCallbacks = []

        self.initialize()

    @property
    def get_id(self):
        """Returns unique camera/nvr identifier."""
        return self.cam_id

    @property
    def get_name(self):
        """Return camera/nvr name."""
        return self.name

    @property
    def get_type(self):
        """Return device type."""
        return self.device_type

    @property
    def current_event_states(self):
        """Return Event states dictionary"""
        return self.event_states

    @property
    def current_motion_detection_state(self):
        """Return current state of motion detection property"""
        return self.motion_detection

    def get_motion_detection(self):
        """Fetch current motion state from camera"""
        url = ('%s/ISAPI/System/Video/inputs/'
               'channels/1/motionDetection') % self.root_url

        try:
            response = self.hik_request.get(url, timeout=CONNECT_TIMEOUT)
        except (requests.exceptions.RequestException,
                requests.exceptions.ConnectionError) as err:
            _LOGGING.error('Unable to fetch MotionDetection, error: %s', err)
            self.motion_detection = None
            return self.motion_detection

        if response.status_code == requests.codes.unauthorized:
            _LOGGING.error('Authentication failed')
            self.motion_detection = None
            return self.motion_detection

        if response.status_code != requests.codes.ok:
            # If we didn't receive 200, abort
            _LOGGING.debug('Unable to fetch motion detection.')
            self.motion_detection = None
            return self.motion_detection

        try:
            tree = ET.fromstring(response.text)
            ET.register_namespace("", self.namespace)
            enabled = tree.find(self.element_query('enabled'))

            if enabled is not None:
                self._motion_detection_xml = tree
            self.motion_detection = {
                'true': True,
                'false': False
            }[enabled.text]
            return self.motion_detection

        except AttributeError as err:
            _LOGGING.error('Entire response: %s', response.text)
            _LOGGING.error('There was a problem: %s', err)
            self.motion_detection = None
            return self.motion_detection

    def enable_motion_detection(self):
        """Enable motion detection"""
        self._set_motion_detection(True)

    def disable_motion_detection(self):
        """Disable motion detection"""
        self._set_motion_detection(False)

    def _set_motion_detection(self, enable):
        """Set desired motion detection state on camera"""
        url = ('%s/ISAPI/System/Video/inputs/'
               'channels/1/motionDetection') % self.root_url

        enabled = self._motion_detection_xml.find(
            self.element_query('enabled'))
        if enabled is None:
            _LOGGING.error("Couldn't find 'enabled' in the xml")
            _LOGGING.error('XML: %s', ET.tostring(self._motion_detection_xml))
            return

        enabled.text = 'true' if enable else 'false'
        xml = ET.tostring(self._motion_detection_xml)

        try:
            response = self.hik_request.put(url,
                                            data=xml,
                                            timeout=CONNECT_TIMEOUT)
        except (requests.exceptions.RequestException,
                requests.exceptions.ConnectionError) as err:
            _LOGGING.error('Unable to set MotionDetection, error: %s', err)
            return

        if response.status_code == requests.codes.unauthorized:
            _LOGGING.error('Authentication failed')
            return

        if response.status_code != requests.codes.ok:
            # If we didn't receive 200, abort
            _LOGGING.error('Unable to set motion detection: %s', response.text)

        self.motion_detection = enable

    def add_update_callback(self, callback, sensor):
        """Register as callback for when a matching device sensor changes."""
        self._updateCallbacks.append([callback, sensor])
        _LOGGING.debug('Added update callback to %s on %s', callback, sensor)

    def _do_update_callback(self, msg):
        """Call registered callback functions."""
        for callback, sensor in self._updateCallbacks:
            if sensor == msg:
                _LOGGING.debug('Update callback %s for sensor %s', callback,
                               sensor)
                callback(msg)

    def element_query(self, element):
        """Build tree query for a given element."""
        return '{%s}%s' % (self.namespace, element)

    def initialize(self):
        """Initialize deviceInfo and available events."""
        device_info = self.get_device_info()

        if device_info is None:
            self.name = None
            self.cam_id = None
            self.event_states = None
            return

        for key in device_info:
            if key == 'deviceName':
                self.name = device_info[key]
            elif key == 'deviceID':
                if len(device_info[key]) > 10:
                    self.cam_id = device_info[key]
                else:
                    self.cam_id = uuid.uuid4()

        events_available = self.get_event_triggers()
        if events_available:
            for event, channel_list in events_available.items():
                for channel in channel_list:
                    try:
                        self.event_states.setdefault(
                            SENSOR_MAP[event.lower()], []).append(
                                [False, channel, 0,
                                 datetime.datetime.now()])
                    except KeyError:
                        # Sensor type doesn't have a known friendly name
                        # We can't reliably handle it at this time...
                        _LOGGING.warning('Sensor type "%s" is unsupported.',
                                         event)

            _LOGGING.debug('Initialized Dictionary: %s', self.event_states)
        else:
            _LOGGING.debug('No Events available in dictionary.')

        self.get_motion_detection()

    def get_event_triggers(self):
        """
        Returns dict of supported events.
        Key = Event Type
        List = Channels that have that event activated
        """
        events = {}
        nvrflag = False
        event_xml = []

        url = '%s/ISAPI/Event/triggers' % self.root_url

        try:
            response = self.hik_request.get(url, timeout=CONNECT_TIMEOUT)
            if response.status_code == requests.codes.not_found:
                # Try alternate URL for triggers
                _LOGGING.debug('Using alternate triggers URL.')
                url = '%s/Event/triggers' % self.root_url
                response = self.hik_request.get(url)

        except (requests.exceptions.RequestException,
                requests.exceptions.ConnectionError) as err:
            _LOGGING.error('Unable to fetch events, error: %s', err)
            return None

        if response.status_code != 200:
            # If we didn't recieve 200, abort
            return None

        # pylint: disable=too-many-nested-blocks
        try:
            content = ET.fromstring(response.text)

            if content[0].find(self.element_query('EventTrigger')):
                event_xml = content[0].findall(
                    self.element_query('EventTrigger'))
            elif content.find(self.element_query('EventTrigger')):
                # This is either an NVR or a rebadged camera
                event_xml = content.findall(self.element_query('EventTrigger'))

            for eventtrigger in event_xml:
                ettype = eventtrigger.find(self.element_query('eventType'))
                # Catch empty xml defintions
                if ettype is None:
                    break
                etnotify = eventtrigger.find(
                    self.element_query('EventTriggerNotificationList'))

                etchannel = None
                etchannel_num = 0

                for node_name in CHANNEL_NAMES:
                    etchannel = eventtrigger.find(
                        self.element_query(node_name))
                    if etchannel is not None:
                        try:
                            # Need to make sure this is actually a number
                            etchannel_num = int(etchannel.text)
                            if etchannel_num > 1:
                                # Must be an nvr
                                nvrflag = True
                            break
                        except ValueError:
                            # Field must not be an integer
                            pass

                if etnotify:
                    for notifytrigger in etnotify:
                        ntype = notifytrigger.find(
                            self.element_query('notificationMethod'))
                        if ntype.text == 'center' or ntype.text == 'HTTP':
                            """
                            If we got this far we found an event that we want
                            to track.
                            """
                            events.setdefault(ettype.text, []) \
                                .append(etchannel_num)

        except (AttributeError, ET.ParseError) as err:
            _LOGGING.error('There was a problem finding an element: %s', err)
            return None

        if nvrflag:
            self.device_type = NVR_DEVICE
        else:
            self.device_type = CAM_DEVICE
        _LOGGING.debug('Processed %s as %s Device.', self.cam_id,
                       self.device_type)

        _LOGGING.debug('Found events: %s', events)
        self.hik_request.close()
        return events

    def get_device_info(self):
        """Parse deviceInfo into dictionary."""
        device_info = {}
        url = '%s/ISAPI/System/deviceInfo' % self.root_url
        using_digest = False

        try:
            response = self.hik_request.get(url, timeout=CONNECT_TIMEOUT)
            if response.status_code == requests.codes.unauthorized:
                _LOGGING.debug('Basic authentication failed. Using digest.')
                self.hik_request.auth = HTTPDigestAuth(self.usr, self.pwd)
                using_digest = True
                response = self.hik_request.get(url)

            if response.status_code == requests.codes.not_found:
                # Try alternate URL for deviceInfo
                _LOGGING.debug('Using alternate deviceInfo URL.')
                url = '%s/System/deviceInfo' % self.root_url
                response = self.hik_request.get(url)
                # Seems to be difference between camera and nvr, they can't seem to
                # agree if they should 404 or 401 first
                if not using_digest and response.status_code == requests.codes.unauthorized:
                    _LOGGING.debug(
                        'Basic authentication failed. Using digest.')
                    self.hik_request.auth = HTTPDigestAuth(self.usr, self.pwd)
                    using_digest = True
                    response = self.hik_request.get(url)

        except (requests.exceptions.RequestException,
                requests.exceptions.ConnectionError) as err:
            _LOGGING.error('Unable to fetch deviceInfo, error: %s', err)
            return None

        if response.status_code == requests.codes.unauthorized:
            _LOGGING.error('Authentication failed')
            return None

        if response.status_code != requests.codes.ok:
            # If we didn't receive 200, abort
            _LOGGING.debug('Unable to fetch device info.')
            return None

        try:
            tree = ET.fromstring(response.text)
            # Try to fetch namespace from XML
            nmsp = tree.tag.split('}')[0].strip('{')
            self.namespace = nmsp if nmsp.startswith('http') else XML_NAMESPACE
            _LOGGING.debug('Using Namespace: %s', self.namespace)

            for item in tree:
                tag = item.tag.split('}')[1]
                device_info[tag] = item.text

            return device_info

        except AttributeError as err:
            _LOGGING.error('Entire response: %s', response.text)
            _LOGGING.error('There was a problem: %s', err)
            return None

    def watchdog_handler(self):
        """Take care of threads if wachdog expires."""
        _LOGGING.debug('%s Watchdog expired. Resetting connection.', self.name)
        self.watchdog.stop()
        self.reset_thrd.set()

    def disconnect(self):
        """Disconnect from event stream."""
        _LOGGING.debug('Disconnecting from stream: %s', self.name)
        self.kill_thrd.set()
        self.thrd.join()
        _LOGGING.debug('Event stream thread for %s is stopped', self.name)
        self.kill_thrd.clear()

    def start_stream(self):
        """Start thread to process event stream."""
        # self.watchdog.start()
        self.thrd.start()

    def alert_stream(self, reset_event, kill_event):
        """Open event stream."""
        _LOGGING.debug('Stream Thread Started: %s, %s', self.name, self.cam_id)
        start_event = False
        parse_string = ""
        fail_count = 0

        url = '%s/ISAPI/Event/notification/alertStream' % self.root_url

        # pylint: disable=too-many-nested-blocks
        while True:

            try:
                stream = self.hik_request.get(url,
                                              stream=True,
                                              timeout=(CONNECT_TIMEOUT,
                                                       READ_TIMEOUT))
                if stream.status_code == requests.codes.not_found:
                    # Try alternate URL for stream
                    url = '%s/Event/notification/alertStream' % self.root_url
                    stream = self.hik_request.get(url, stream=True)

                if stream.status_code != requests.codes.ok:
                    raise ValueError('Connection unsucessful.')
                else:
                    _LOGGING.debug('%s Connection Successful.', self.name)
                    fail_count = 0
                    self.watchdog.start()

                for line in stream.iter_lines():
                    # _LOGGING.debug('Processing line from %s', self.name)
                    # filter out keep-alive new lines
                    if line:
                        str_line = line.decode("utf-8", "ignore")
                        # New events start with --boundry
                        if str_line.find('<EventNotificationAlert') != -1:
                            # Start of event message
                            start_event = True
                            parse_string += str_line
                        elif str_line.find('</EventNotificationAlert>') != -1:
                            # Message end found found
                            parse_string += str_line
                            start_event = False
                            if parse_string:
                                try:
                                    tree = ET.fromstring(parse_string)
                                    self.process_stream(tree)
                                    self.update_stale()
                                except ET.ParseError as err:
                                    _LOGGING.warning(
                                        'XML parse error in stream.')
                                parse_string = ""
                        else:
                            if start_event:
                                parse_string += str_line

                    if kill_event.is_set():
                        # We were asked to stop the thread so lets do so.
                        break
                    elif reset_event.is_set():
                        # We need to reset the connection.
                        raise ValueError('Watchdog failed.')

                if kill_event.is_set():
                    # We were asked to stop the thread so lets do so.
                    _LOGGING.debug('Stopping event stream thread for %s',
                                   self.name)
                    self.watchdog.stop()
                    self.hik_request.close()
                    return
                elif reset_event.is_set():
                    # We need to reset the connection.
                    raise ValueError('Watchdog failed.')

            except (ValueError, requests.exceptions.ConnectionError,
                    requests.exceptions.ChunkedEncodingError) as err:
                fail_count += 1
                reset_event.clear()
                _LOGGING.warning(
                    '%s Connection Failed (count=%d). Waiting %ss. Err: %s',
                    self.name, fail_count, (fail_count * 5) + 5, err)
                parse_string = ""
                self.watchdog.stop()
                self.hik_request.close()
                time.sleep(5)
                self.update_stale()
                time.sleep(fail_count * 5)
                continue

    def process_stream(self, tree):
        """Process incoming event stream packets."""
        try:
            etype = SENSOR_MAP[tree.find(
                self.element_query('eventType')).text.lower()]
            estate = tree.find(self.element_query('eventState')).text
            echid = tree.find(self.element_query('channelID'))
            if echid is None:
                # Some devices use a different key
                echid = tree.find(self.element_query('dynChannelID'))
            echid = int(echid.text)
            ecount = tree.find(self.element_query('activePostCount')).text
        except (AttributeError, KeyError, IndexError) as err:
            _LOGGING.error('Problem finding attribute: %s', err)
            return

        # Take care of keep-alive
        if len(etype) > 0 and etype == 'Video Loss':
            self.watchdog.pet()

        # Track state if it's in the event list.
        if len(etype) > 0:
            state = self.fetch_attributes(etype, echid)
            if state:
                # Determine if state has changed
                # If so, publish, otherwise do nothing
                estate = (estate == 'active')
                old_state = state[0]
                attr = [estate, echid, int(ecount), datetime.datetime.now()]
                self.update_attributes(etype, echid, attr)

                if estate != old_state:
                    self.publish_changes(etype, echid)
                self.watchdog.pet()

    def update_stale(self):
        """Update stale active statuses"""
        # Some events don't post an inactive XML, only active.
        # If we don't get an active update for 5 seconds we can
        # assume the event is no longer active and update accordingly.
        for etype, echannels in self.event_states.items():
            for eprop in echannels:
                if eprop[3] is not None:
                    sec_elap = ((datetime.datetime.now() -
                                 eprop[3]).total_seconds())
                    # print('Seconds since last update: {}'.format(sec_elap))
                    if sec_elap > 5 and eprop[0] is True:
                        _LOGGING.debug('Updating stale event %s on CH(%s)',
                                       etype, eprop[1])
                        attr = [
                            False, eprop[1], eprop[2],
                            datetime.datetime.now()
                        ]
                        self.update_attributes(etype, eprop[1], attr)
                        self.publish_changes(etype, eprop[1])

    def publish_changes(self, etype, echid):
        """Post updates for specified event type."""
        _LOGGING.debug('%s Update: %s, %s', self.name, etype,
                       self.fetch_attributes(etype, echid))
        signal = 'ValueChanged.{}'.format(self.cam_id)
        sender = '{}.{}'.format(etype, echid)
        if dispatcher:
            dispatcher.send(signal=signal, sender=sender)

        self._do_update_callback('{}.{}.{}'.format(self.cam_id, etype, echid))

    def fetch_attributes(self, event, channel):
        """Returns attribute list for a given event/channel."""
        try:
            for sensor in self.event_states[event]:
                if sensor[1] == int(channel):
                    return sensor
        except KeyError:
            return None

    def update_attributes(self, event, channel, attr):
        """Update attribute list for current event/channel."""
        try:
            for i, sensor in enumerate(self.event_states[event]):
                if sensor[1] == int(channel):
                    self.event_states[event][i] = attr
        except KeyError:
            _LOGGING.debug('Error updating attributes for: (%s, %s)', event,
                           channel)
Ejemplo n.º 4
0
class HikCamera(object):
    """Creates a new Hikvision api device."""
    def __init__(self, host=None, port=DEFAULT_PORT, usr=None, pwd=None):
        """Initialize device."""

        _LOGGING.debug("pyHik %s initializing new hikvision device at: %s",
                       __version__, host)

        self.event_states = {}

        self.watchdog = Watchdog(300.0, self.watchdog_handler)

        self.namespace = XML_NAMESPACE

        if not host:
            _LOGGING.error('Host not specified! Cannot continue.')
            return

        self.host = host
        self.usr = usr
        self.pwd = pwd
        self.cam_id = 0
        self.name = ''
        self.device_type = None

        self.root_url = '{}:{}'.format(host, port)

        # Build requests session for main thread calls
        # Default to basic authentication. It will change to digest inside
        # get_device_info if basic fails
        self.hik_request = requests.Session()
        self.hik_request.auth = (usr, pwd)
        self.hik_request.timeout = 5
        self.hik_request.headers.update(DEFAULT_HEADERS)

        # Define event stream processing thread
        self.kill_thrd = threading.Event()
        self.reset_thrd = threading.Event()
        self.thrd = threading.Thread(target=self.alert_stream,
                                     args=(
                                         self.reset_thrd,
                                         self.kill_thrd,
                                     ))
        self.thrd.daemon = False

        # Callbacks
        self._updateCallbacks = []

        self.initialize()

    @property
    def get_id(self):
        """Returns unique camera/nvr identifier."""
        return self.cam_id

    @property
    def get_name(self):
        """Return camera/nvr name."""
        return self.name

    @property
    def get_type(self):
        """Return device type."""
        return self.device_type

    @property
    def current_event_states(self):
        """Return Event states dictionary"""
        return self.event_states

    def add_update_callback(self, callback, sensor):
        """Register as callback for when a matching device sensor changes."""
        self._updateCallbacks.append([callback, sensor])
        _LOGGING.debug('Added update callback to %s on %s', callback, sensor)

    def _do_update_callback(self, msg):
        """Call registered callback functions."""
        for callback, sensor in self._updateCallbacks:
            if sensor == msg:
                _LOGGING.debug('Update callback %s for sensor %s', callback,
                               sensor)
                callback(msg)

    '''

    '''

    def element_query(self, elements, namespace=None):
        """
            Build tree query for a given element.
            Pass in the path either as a string like
                elements = 'EventTrigger'
            or as an array like:
                elements = ['some/', 'path/', 'here']
        """
        if namespace is None:
            namespace = self.namespace

        # if only one element is passed in as string wrap it as a list
        if not isinstance(elements, list):
            elements = [elements]

        result = ''
        for element in elements:
            result += '{%s}%s' % (namespace, element)

        return result

    def initialize(self):
        """Initialize deviceInfo and available events."""
        device_info = self.get_device_info()

        if device_info is None:
            self.name = None
            self.cam_id = None
            self.event_states = None
            return

        for key in device_info:
            if key == 'deviceName':
                self.name = device_info[key]
            elif key == 'deviceID':
                if len(device_info[key]) > 10:
                    self.cam_id = device_info[key]
                else:
                    self.cam_id = uuid.uuid4()

        events_available = self.get_event_triggers()
        if events_available:
            for event, channel_list in events_available.items():
                for channel in channel_list:
                    try:
                        self.event_states.setdefault(
                            SENSOR_MAP[event], []).append(
                                [False, channel, 0,
                                 datetime.datetime.now()])
                    except KeyError:
                        # Sensor type doesn't have a known friendly name
                        # We can't reliably handle it at this time...
                        _LOGGING.warning('Sensor type "%s" is unsupported.',
                                         event)

            _LOGGING.debug('Initialized Dictionary: %s', self.event_states)
        else:
            _LOGGING.debug('No Events available in dictionary.')

    def get_event_triggers(self):
        """
        Returns dict of supported events.
        Key = Event Type
        List = Channels that have that event activated
        """
        events = {}
        nvrflag = False

        url = '%s/ISAPI/Event/triggers' % self.root_url

        try:
            response = self.hik_request.get(url)
            if response.status_code == requests.codes.not_found:
                # Try alternate URL for triggers
                _LOGGING.debug('Using alternate triggers URL.')
                url = '%s/Event/triggers' % self.root_url
                response = self.hik_request.get(url)

        except requests.exceptions.RequestException as err:
            _LOGGING.error('Unable to fetch events, error: %s', err)
            return None

        if response.status_code != 200:
            # If we didn't recieve 200, abort
            return None

        # pylint: disable=too-many-nested-blocks
        try:
            content = ET.fromstring(response.text)

            if content[0].find(self.element_query('EventTrigger')):
                event_xml = content[0].findall(
                    self.element_query('EventTrigger'))
            elif content.find(self.element_query('EventTrigger')):
                # This is either an NVR or a rebadged camera
                event_xml = content.findall(self.element_query('EventTrigger'))

            for eventtrigger in event_xml:
                ettype = eventtrigger.find(self.element_query('eventType'))
                # Catch empty xml defintions
                if ettype is None:
                    break
                etnotify = eventtrigger.find(
                    self.element_query('EventTriggerNotificationList'))

                etchannel = None
                etchannel_num = 0

                for node_name in CHANNEL_NAMES:
                    etchannel = eventtrigger.find(
                        self.element_query(node_name))
                    if etchannel is not None:
                        try:
                            # Need to make sure this is actually a number
                            etchannel_num = int(etchannel.text)
                            if etchannel_num > 1:
                                # Must be an nvr
                                nvrflag = True
                            break
                        except ValueError:
                            # Field must not be an integer
                            pass

                if etnotify:
                    for notifytrigger in etnotify:
                        ntype = notifytrigger.find(
                            self.element_query('notificationMethod'))
                        if ntype.text == 'center' or ntype.text == 'HTTP':
                            """
                            If we got this far we found an event that we want
                            to track.
                            """
                            events.setdefault(ettype.text, []) \
                                .append(etchannel_num)

        except (AttributeError, ET.ParseError) as err:
            _LOGGING.error('There was a problem finding an element: %s', err)
            return None

        if nvrflag:
            self.device_type = NVR_DEVICE
        else:
            self.device_type = CAM_DEVICE
        _LOGGING.debug('Processed %s as %s Device.', self.cam_id,
                       self.device_type)

        _LOGGING.debug('Found events: %s', events)
        self.hik_request.close()
        return events

    def get_device_info(self):
        """Parse deviceInfo into dictionary."""
        device_info = {}
        url = '%s/ISAPI/System/deviceInfo' % self.root_url
        using_digest = False

        try:
            print("Trying to connect!!")
            response = self.hik_request.get(url)
            if response.status_code == requests.codes.unauthorized:
                print("Basic authentication failed. Using digest 1")
                _LOGGING.debug('Basic authentication failed. Using digest.')
                self.hik_request.auth = HTTPDigestAuth(self.usr, self.pwd)
                using_digest = True
                response = self.hik_request.get(url)

            if response.status_code == requests.codes.not_found:
                # Try alternate URL for deviceInfo
                _LOGGING.debug('Using alternate deviceInfo URL.')
                url = '%s/System/deviceInfo' % self.root_url
                response = self.hik_request.get(url)
                # Seems to be difference between camera and nvr, they can't seem to
                # agree if they should 404 or 401 first
                if not using_digest and response.status_code == requests.codes.unauthorized:
                    print("Basic authentication failed. Using digest 2")
                    _LOGGING.debug(
                        'Basic authentication failed. Using digest.')
                    self.hik_request.auth = HTTPDigestAuth(self.usr, self.pwd)
                    using_digest = True
                    response = self.hik_request.get(url)

        except requests.exceptions.RequestException as err:
            _LOGGING.error('Unable to fetch deviceInfo, error: %s', err)
            return None

        if response.status_code == requests.codes.unauthorized:
            _LOGGING.error('Authentication failed')
            return None

        if response.status_code != requests.codes.ok:
            # If we didn't receive 200, abort
            _LOGGING.debug('Unable to fetch device info.')
            return None

        try:
            tree = ET.fromstring(response.text)
            # Try to fetch namespace from XML
            nmsp = tree.tag.split('}')[0].strip('{')
            self.namespace = nmsp if nmsp.startswith('http') else XML_NAMESPACE
            _LOGGING.debug('Using Namespace: %s', self.namespace)

            for item in tree:
                tag = item.tag.split('}')[1]
                device_info[tag] = item.text

            return device_info

        except AttributeError as err:
            _LOGGING.error('Entire response: %s', response.text)
            _LOGGING.error('There was a problem: %s', err)
            return None

    def watchdog_handler(self):
        """Take care of threads if wachdog expires."""
        _LOGGING.debug('%s Watchdog expired. Resetting connection.', self.name)
        self.watchdog.stop()
        self.reset_thrd.set()

    def disconnect(self):
        """Disconnect from event stream."""
        _LOGGING.debug('Disconnecting from stream: %s', self.name)
        self.kill_thrd.set()
        self.thrd.join()
        _LOGGING.debug('Event stream thread for %s is stopped', self.name)
        self.kill_thrd.clear()

    def start_stream(self):
        """Start thread to process event stream."""
        # self.watchdog.start()
        self.thrd.start()

    def alert_stream(self, reset_event, kill_event):
        """Open event stream."""
        _LOGGING.debug('Stream Thread Started: %s, %s', self.name, self.cam_id)
        start_event = False
        parse_string = ""
        fail_count = 0

        url = '%s/ISAPI/Event/notification/alertStream' % self.root_url

        # pylint: disable=too-many-nested-blocks
        while True:

            try:
                stream = self.hik_request.get(url, stream=True)
                if stream.status_code == requests.codes.not_found:
                    # Try alternate URL for stream
                    url = '%s/Event/notification/alertStream' % self.root_url
                    stream = self.hik_request.get(url, stream=True)

                if stream.status_code != requests.codes.ok:
                    raise ValueError('Connection unsucessful.')
                else:
                    _LOGGING.debug('%s Connection Successful.', self.name)
                    fail_count = 0
                    self.watchdog.start()

                for line in stream.iter_lines():
                    # _LOGGING.debug('Processing line from %s', self.name)
                    # filter out keep-alive new lines
                    if line:
                        str_line = line.decode("utf-8")
                        # New events start with --boundry
                        if str_line.find('Content-Length') != -1:
                            # Start of event message
                            start_event = True
                        elif str_line.find('</EventNotificationAlert>') != -1:
                            # Message end found found
                            parse_string += str_line
                            start_event = False
                            if parse_string:
                                try:
                                    tree = ET.fromstring(parse_string)
                                    self.process_stream(tree)
                                    self.update_stale()
                                except ET.ParseError as e:
                                    sys.stderr.write(parse_string)
                                    sys.stderr.write("\n")
                                    sys.stderr.write(str(e))
                                    sys.stderr.flush()
                                parse_string = ""
                        else:
                            if start_event:
                                parse_string += str_line

                    if kill_event.is_set():
                        # We were asked to stop the thread so lets do so.
                        break
                    elif reset_event.is_set():
                        # We need to reset the connection.
                        raise ValueError('Watchdog failed.')

                if kill_event.is_set():
                    # We were asked to stop the thread so lets do so.
                    _LOGGING.debug('Stopping event stream thread for %s',
                                   self.name)
                    self.watchdog.stop()
                    self.hik_request.close()
                    return
                elif reset_event.is_set():
                    # We need to reset the connection.
                    raise ValueError('Watchdog failed.')

            except (ValueError,
                    requests.exceptions.ChunkedEncodingError) as err:
                fail_count += 1
                reset_event.clear()
                _LOGGING.warning('%s Connection Failed. Waiting %ss. Err: %s',
                                 self.name, (fail_count * 5) + 5, err)
                parse_string = ""
                self.watchdog.stop()
                self.hik_request.close()
                time.sleep(5)
                self.update_stale()
                time.sleep(fail_count * 5)
                continue

    def process_stream(self, tree):
        """Process incoming event stream packets."""
        try:
            etype = SENSOR_MAP[tree.find(self.element_query('eventType')).text]
            estate = tree.find(self.element_query('eventState')).text
            echid = tree.find(self.element_query('channelID'))
            if echid is None:
                # Some devices use a different key
                echid = tree.find(self.element_query('dynChannelID'))
            echid = int(echid.text)
            ecount = tree.find(self.element_query('activePostCount')).text
        except (AttributeError, KeyError, IndexError) as err:
            _LOGGING.error('Problem finding attribute: %s', err)
            return

        # Take care of keep-alive
        if len(etype) > 0 and etype == 'Video Loss':
            self.watchdog.pet()

        # Track state if it's in the event list.
        if len(etype) > 0:
            state = self.fetch_attributes(etype, echid)
            if state:
                # Determine if state has changed
                # If so, publish, otherwise do nothing
                estate = (estate == 'active')
                old_state = state[0]
                attr = [estate, echid, int(ecount), datetime.datetime.now()]
                self.update_attributes(etype, echid, attr)

                if estate != old_state:
                    self.publish_changes(etype, echid)
                self.watchdog.pet()

    def update_stale(self):
        """Update stale active statuses"""
        # Some events don't post an inactive XML, only active.
        # If we don't get an active update for 5 seconds we can
        # assume the event is no longer active and update accordingly.
        for etype, echannels in self.event_states.items():
            for eprop in echannels:
                if eprop[3] is not None:
                    sec_elap = ((datetime.datetime.now() -
                                 eprop[3]).total_seconds())
                    # print('Seconds since last update: {}'.format(sec_elap))
                    if sec_elap > 5 and eprop[0] is True:
                        _LOGGING.debug('Updating stale event %s on CH(%s)',
                                       etype, eprop[1])
                        attr = [
                            False, eprop[1], eprop[2],
                            datetime.datetime.now()
                        ]
                        self.update_attributes(etype, eprop[1], attr)
                        self.publish_changes(etype, eprop[1])

    def publish_changes(self, etype, echid):
        """Post updates for specified event type."""
        _LOGGING.debug('%s Update: %s, %s', self.name, etype,
                       self.fetch_attributes(etype, echid))
        signal = 'ValueChanged.{}'.format(self.cam_id)
        sender = '{}.{}'.format(etype, echid)
        if dispatcher:
            dispatcher.send(signal=signal, sender=sender)

        self._do_update_callback('{}.{}.{}'.format(self.cam_id, etype, echid))

    def fetch_attributes(self, event, channel):
        """Returns attribute list for a given event/channel."""
        try:
            for sensor in self.event_states[event]:
                if sensor[1] == int(channel):
                    return sensor
        except KeyError:
            return None

    def update_attributes(self, event, channel, attr):
        """Update attribute list for current event/channel."""
        try:
            for i, sensor in enumerate(self.event_states[event]):
                if sensor[1] == int(channel):
                    self.event_states[event][i] = attr
        except KeyError:
            _LOGGING.debug('Error updating attributes for: (%s, %s)', event,
                           channel)

    # private method
    def searchRecording(self, start_time, end_time, limit=200):
        url = '%s/ISAPI/ContentMgmt/search' % self.root_url

        ET.register_namespace("", XML_NAMESPACE_ISAPI)
        tree = ET.parse('./pyHik/pyhik/XmlTemplateSearch.xml').getroot()

        tree.find(self.element_query(
            ['searchID'], XML_NAMESPACE_ISAPI)).text = str(uuid.uuid4())
        tree.find(
            self.element_query(['trackIDList/', 'trackID'],
                               XML_NAMESPACE_ISAPI)).text = '101'
        tree.find(
            self.element_query(['timeSpanList/', 'timeSpan/', 'startTime'],
                               XML_NAMESPACE_ISAPI)).text = start_time
        tree.find(
            self.element_query(['timeSpanList/', 'timeSpan/', 'endTime'],
                               XML_NAMESPACE_ISAPI)).text = end_time
        tree.find(self.element_query(['maxResults'],
                                     XML_NAMESPACE_ISAPI)).text = str(limit)

        xml = ET.tostring(tree)

        return self.hik_request.post(url, data=xml).text

    '''
    Get a list of all recordings between the two dates.
    start_time = '2018-01-03T00:00:00Z'
    end_time   = '2018-01-03T23:59:59Z'

    Each element in the returned list contains the following attributes:
    startTime, endTime, codec, url
    '''

    def getRecordingList(self, start_time, end_time):
        chunk_size = 200

        ET.register_namespace("", XML_NAMESPACE)
        #root = ET.parse('./search.xml').getroot()

        recordings_data = []
        while True:
            xml_data = self.searchRecording(start_time, end_time)
            #print(xml_data)
            root = ET.fromstring(xml_data)
            recordings_data_new = root.findall(
                self.element_query(['matchList/', 'searchMatchItem'],
                                   XML_NAMESPACE))
            recordings_data.extend(recordings_data_new)

            recordings_data_new_length = len(recordings_data_new)
            print("Fetched: {}".format(recordings_data_new_length))
            if recordings_data_new_length < chunk_size:
                break
            else:
                last_end_time = recordings_data_new[-1].find(
                    self.element_query(['timeSpan/', 'endTime'])).text
                print("Changing current start time: {} to new start time: {}".
                      format(start_time, last_end_time))
                start_time = last_end_time

        print("Fetched total: {}".format(len(recordings_data)))

        recordings = []

        for recording in recordings_data:
            startTime = recording.find(
                self.element_query(['timeSpan/', 'startTime'])).text
            endTime = recording.find(
                self.element_query(['timeSpan/', 'endTime'])).text
            codec = recording.find(
                self.element_query(['mediaSegmentDescriptor/',
                                    'codecType'])).text
            url = recording.find(
                self.element_query(['mediaSegmentDescriptor/',
                                    'playbackURI'])).text

            recordings.append({
                'startTime': startTime,
                'endTime': endTime,
                'codec': codec,
                'url': url
            })

        return recordings

    '''
    Download a video recording
    playbackURI = Url to download the video from
    filename    = Where to store the file on the filesystem
    '''

    def downloadVideo(self, playbackURI, filename):
        url = '%s/ISAPI/ContentMgmt/download' % self.root_url

        tree = ET.parse('./pyHik/pyhik/XmlTemplateDownload.xml').getroot()
        tree.find('playbackURI').text = playbackURI
        xml = ET.tostring(tree)

        r = self.hik_request.get(url, data=xml)

        if r.status_code == 200:
            with open(filename, 'wb') as f:
                f.write(r.content)
        elif 'content-type' in r.headers and r.headers[
                'content-type'] == 'ContentType: application/xml':
            print("Error while downloading video files!")
            print(r.content)

        # Retrieve HTTP meta-data
        _LOGGING.debug('%s Status Code: %s, ContentType: %s Encoding: %s',
                       self.name, r.status_code, r.headers['content-type'],
                       r.encoding)