├── README.md └── dahua_mqtt.py /README.md: -------------------------------------------------------------------------------- 1 | # Your support 2 | Buy Me A Coffee 3 | -------------------------------------------------------------------------------- /dahua_mqtt.py: -------------------------------------------------------------------------------- 1 | """ 2 | Dahua IP Camera events to MQTT app. Implemented from: https://github.com/johnnyletrois/dahua-watch 3 | 4 | Example configuration: 5 | 6 | DahuaMQTT: 7 | class: DahuaMQTT 8 | module: dahua_mqtt 9 | cameras: 10 | - host: 192.168.0.1 11 | port: 80 12 | user: user 13 | pass: pass 14 | topic: cameras/1 15 | retain: true 16 | events: VideoMotion,VideoBlind,VideoLoss,AlarmLocal,.... 17 | - host: 192.168.0.2 18 | port: 80 19 | user: user 20 | pass: pass 21 | topic: cameras/2 22 | events: VideoMotion,VideoBlind,VideoLoss,AlarmLocal,.... 23 | 24 | App sends two MQTT topics: 25 | First MQTT topic will be: cameras/1/, ex: cameras/1/VideoMotion and payload will be action: Start or Stop 26 | Second MQTT topic will be: cameras/1, ex: cameras/1 and payload will be data received from camera in JSON format 27 | 28 | According to the API docs, these events are available: (availability depends on your device and firmware) 29 | VideoMotion: motion detection event 30 | VideoLoss: video loss detection event 31 | VideoBlind: video blind detection event. 32 | AlarmLocal: alarm detection event. 33 | CrossLineDetection: tripwire event 34 | CrossRegionDetection: intrusion event 35 | LeftDetection: abandoned object detection 36 | TakenAwayDetection: missing object detection 37 | VideoAbnormalDetection: scene change event 38 | FaceDetection: face detect event 39 | AudioMutation: intensity change 40 | AudioAnomaly: input abnormal 41 | VideoUnFocus: defocus detect event 42 | WanderDetection: loitering detection event 43 | RioterDetection: People Gathering event 44 | ParkingDetection: parking detection event 45 | MoveDetection: fast moving event 46 | MDResult: motion detection data reporting event. The motion detect window contains 18 rows and 22 columns. The event info contains motion detect data with mask of every row. 47 | HeatImagingTemper: temperature alarm event 48 | """ 49 | 50 | import appdaemon.plugins.hass.hassapi as hass 51 | import socket 52 | import pycurl 53 | import time 54 | import threading 55 | import json 56 | from threading import Thread 57 | 58 | URL_TEMPLATE = "http://{host}:{port}/cgi-bin/eventManager.cgi?action=attach&codes=%5B{events}%5D" 59 | 60 | 61 | class DahuaMQTT(hass.Hass): 62 | 63 | proc = None 64 | cameras = [] 65 | curl_multiobj = pycurl.CurlMulti() 66 | num_curlobj = 0 67 | kill_thread = False 68 | 69 | def initialize(self): 70 | 71 | for camera in self.args["cameras"]: 72 | dahuacam = DahuaCamera(self, camera) 73 | self.cameras.append(dahuacam) 74 | url = URL_TEMPLATE.format(**camera) 75 | 76 | curlobj = pycurl.Curl() 77 | dahuacam.curlobj = curlobj 78 | 79 | curlobj.setopt(pycurl.URL, url) 80 | curlobj.setopt(pycurl.CONNECTTIMEOUT, 30) 81 | curlobj.setopt(pycurl.TCP_KEEPALIVE, 1) 82 | curlobj.setopt(pycurl.TCP_KEEPIDLE, 30) 83 | curlobj.setopt(pycurl.TCP_KEEPINTVL, 15) 84 | curlobj.setopt(pycurl.HTTPAUTH, pycurl.HTTPAUTH_DIGEST) 85 | curlobj.setopt(pycurl.USERPWD, "{0}:{1}".format(camera["user"], camera["pass"])) 86 | curlobj.setopt(pycurl.WRITEFUNCTION, dahuacam.on_receive) 87 | 88 | if "ignore_ssl" in self.args and self.args["ignore_ssl"] is True: 89 | curlobj.setopt(pycurl.SSL_VERIFYPEER, 0) 90 | curlobj.setopt(pycurl.SSL_VERIFYHOST, 0) 91 | 92 | self.curl_multiobj.add_handle(curlobj) 93 | self.num_curlobj += 1 94 | 95 | self.log("Starting thread") 96 | self.proc = Thread(target=self.thread_process) 97 | self.proc.daemon = False 98 | self.proc.start() 99 | 100 | def terminate(self): 101 | if self.proc and self.proc.is_alive(): 102 | self.log("Killing thread") 103 | self.kill_thread = True 104 | self.proc.join() 105 | 106 | def thread_process(self): 107 | while not self.kill_thread: 108 | ret, num_handles = self.curl_multiobj.perform() 109 | if ret != pycurl.E_CALL_MULTI_PERFORM: 110 | break 111 | 112 | while not self.kill_thread: 113 | ret = self.curl_multiobj.select(0.1) 114 | if ret == -1: 115 | self.on_timer() 116 | continue 117 | 118 | while not self.kill_thread: 119 | ret, num_handles = self.curl_multiobj.perform() 120 | 121 | if num_handles != self.num_curlobj: 122 | _, success, error = self.curl_multiobj.info_read() 123 | 124 | for curlobj in success: 125 | camera = next(filter(lambda x: x.curlobj == curlobj, self.cameras)) 126 | if camera.reconnect: 127 | continue 128 | 129 | camera.on_disconnect("Success {0}".format(error)) 130 | camera.reconnect = time.time() + 5 131 | 132 | for curlobj, errorno, errorstr in error: 133 | camera = next(filter(lambda x: x.curlobj == curlobj, self.cameras)) 134 | if camera.reconnect: 135 | continue 136 | 137 | camera.on_disconnect("{0} ({1})".format(errorstr, errorno)) 138 | camera.reconnect = time.time() + 5 139 | 140 | for camera in self.cameras: 141 | if camera.reconnect and camera.reconnect < time.time(): 142 | self.curl_multiobj.remove_handle(camera.curlobj) 143 | self.curl_multiobj.add_handle(camera.curlobj) 144 | camera.reconnect = None 145 | 146 | if ret != pycurl.E_CALL_MULTI_PERFORM: 147 | break 148 | 149 | self.log("Thread exited") 150 | 151 | 152 | class DahuaCamera: 153 | 154 | def __init__(self, hass, camera): 155 | self.hass = hass 156 | self.camera = camera 157 | self.curlobj = None 158 | self.connected = None 159 | self.reconnect = None 160 | 161 | self.alarm = None 162 | 163 | def on_alarm(self, state): 164 | 165 | # Convert data from JSON string to JSON object 166 | if "data" in state: 167 | state["data"] = json.loads(state["data"]) 168 | 169 | # Publish two topics 170 | mqtt_data = { 171 | self.camera["topic"]: json.dumps(state), 172 | self.camera["topic"] + state["code"]: state["action"] 173 | } 174 | 175 | for topic, payload in mqtt_data.items(): 176 | topic = topic.strip("/") 177 | self.hass.log("[{0}] Publishing MQTT. topic={1}, payload={2}".format(self.camera["host"], topic, payload)) 178 | self.hass.call_service("mqtt/publish", topic=topic, payload=payload, retain=self.camera["retain"]) 179 | 180 | def on_connect(self): 181 | self.hass.log("[{0}] OnConnect()".format(self.camera["host"])) 182 | self.connected = True 183 | 184 | def on_disconnect(self, reason): 185 | self.hass.log("[{0}] OnDisconnect({1})".format(self.camera["host"], reason)) 186 | self.connected = False 187 | 188 | def on_receive(self, data): 189 | decoded_data = data.decode("utf-8", errors="ignore") 190 | # self.hass.log("[{0}]: {1}".format(self.camera["host"], decoded_data)) 191 | 192 | for line in decoded_data.split("\r\n"): 193 | if line == "HTTP/1.1 200 OK": 194 | self.on_connect() 195 | 196 | if not line.startswith("Code="): 197 | continue 198 | 199 | try: 200 | alarm = dict() 201 | for keyval in line.split(';'): 202 | key, val = keyval.split('=') 203 | alarm[key.lower()] = val 204 | 205 | self.parse_event(alarm) 206 | except Exception as ex: 207 | self.hass.log("Failed to parse: {0}".format(str(ex))) 208 | 209 | def parse_event(self, alarm): 210 | # self.hass.log("[{0}] Parse Event ({1})".format(self.camera["host"], alarm)) 211 | 212 | if alarm["code"] not in self.camera["events"].split(','): 213 | return 214 | 215 | self.on_alarm(alarm) 216 | --------------------------------------------------------------------------------