├── README.md
└── dahua_mqtt.py
/README.md:
--------------------------------------------------------------------------------
1 | # Your support
2 |
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 |
--------------------------------------------------------------------------------