99import homeassistant .helpers .config_validation as cv
1010import voluptuous as vol
1111from homeassistant .components .sensor import PLATFORM_SCHEMA
12- from homeassistant .const import CONF_NAME , CONF_HOST , CONF_PORT , CONF_USERNAME , CONF_PASSWORD
12+ from homeassistant .const import CONF_NAME , CONF_HOST , CONF_PORT , \
13+ CONF_USERNAME , CONF_PASSWORD , CONF_TIMEOUT
1314
1415DOMAIN = "dahua_vto"
1516DAHUA_PROTO_DHIP = 0x5049484400000020
1617DAHUA_HEADER_FORMAT = "<QLLQQ"
1718DAHUA_REALM_DHIP = 268632079 # DHIP REALM Login Challenge
19+ DAHUA_LOGIN_PARAMS = {
20+ "clientType" : "" , "ipAddr" : "(null)" , "loginType" : "Direct" }
1821
1922DEFAULT_NAME = "Dahua VTO"
2023DEFAULT_PORT = 5000
24+ DEFAULT_TIMEOUT = 10
2125
2226_LOGGER = logging .getLogger (__name__ )
2327
2630 vol .Optional (CONF_NAME , default = DEFAULT_NAME ): cv .string ,
2731 vol .Required (CONF_HOST ): cv .string ,
2832 vol .Optional (CONF_PORT , default = DEFAULT_PORT ): cv .positive_int ,
33+ vol .Optional (CONF_TIMEOUT , default = DEFAULT_TIMEOUT ): cv .positive_int ,
2934 vol .Required (CONF_USERNAME ): cv .string ,
3035 vol .Required (CONF_PASSWORD ): cv .string ,
3136})
3237
3338
34- async def async_setup_platform (hass , config , add_entities , discovery_info = None ):
39+ async def async_setup_platform (
40+ hass , config , add_entities , discovery_info = None
41+ ):
3542 """Set up the sensor platform."""
3643 name = config [CONF_NAME ]
3744 entity = DahuaVTO (hass , name , config )
@@ -64,7 +71,49 @@ def __init__(self, hass, name, username, password, on_connection_lost):
6471
6572 def connection_made (self , transport ):
6673 self .transport = transport
67- self .send ({"method" : "global.login" , "params" : {"clientType" : "" , "ipAddr" : "(null)" , "loginType" : "Direct" }})
74+ self .send ({"method" : "global.login" , "params" : DAHUA_LOGIN_PARAMS })
75+
76+ def connection_lost (self , exc ):
77+ if self .heartbeat is not None :
78+ self .heartbeat .cancel ()
79+ self .heartbeat = None
80+ if not self .on_connection_lost .done ():
81+ self .on_connection_lost .set_result (True )
82+
83+ def hashed_password (self , random , realm ):
84+ h = hashlib .md5 (f"{ self .username } :{ realm } :{ self .password } " .encode (
85+ "utf-8" )).hexdigest ().upper ()
86+ return hashlib .md5 (f"{ self .username } :{ random } :{ h } " .encode (
87+ "utf-8" )).hexdigest ().upper ()
88+
89+ def receive (self , message ):
90+ params = message .get ("params" )
91+ error = message .get ("error" )
92+
93+ if error is not None :
94+ if error ["code" ] == DAHUA_REALM_DHIP :
95+ self .sessionId = message ["session" ]
96+ login = DAHUA_LOGIN_PARAMS
97+ login ["userName" ] = self .username
98+ login ["password" ] = self .hashed_password (
99+ params ["random" ], params ["realm" ])
100+ self .send ({"method" : "global.login" , "params" : login })
101+ else :
102+ raise Exception ("{}: {}" .format (
103+ error .get ("code" ), error .get ("message" )))
104+ elif message ["id" ] == 2 :
105+ self .keepAliveInterval = params .get ("keepAliveInterval" )
106+ if self .keepAliveInterval is None :
107+ raise Exception ("keepAliveInterval" )
108+ if self .heartbeat is not None :
109+ raise Exception ("Heartbeat already run" )
110+ self .heartbeat = self .loop .create_task (self .heartbeat_loop ())
111+ self .send ({"method" : "eventManager.attach" ,
112+ "params" : {"codes" : ["All" ]}})
113+ elif message .get ("method" ) == "client.notifyEventStream" :
114+ for message in params .get ("eventList" ):
115+ message ["name" ] = self .name
116+ self .hass .bus .fire (DOMAIN , message )
68117
69118 def data_received (self , data ):
70119 try :
@@ -75,13 +124,13 @@ def data_received(self, data):
75124 if self .chunk_remaining > 0 :
76125 return
77126 elif self .chunk_remaining < 0 :
78- raise Exception (f"Protocol error, remaining bytes { self .chunk_remaining } " )
127+ raise Exception (f"Remaining bytes { self .chunk_remaining } " )
79128 packet = self .chunk
80129 self .chunk = None
81130 else :
82131 header = struct .unpack (DAHUA_HEADER_FORMAT , data [0 :32 ])
83132 if header [0 ] != DAHUA_PROTO_DHIP :
84- raise Exception ("Protocol error, wrong proto" )
133+ raise Exception ("Wrong proto" )
85134 packet = data [32 :].decode ("utf-8" , "ignore" )
86135 if header [4 ] > len (packet ):
87136 self .chunk = packet
@@ -90,53 +139,26 @@ def data_received(self, data):
90139
91140 _LOGGER .debug ("<<< {}" .format (packet .strip ("\n " )))
92141 message = json .loads (packet )
93- message_id = message .get ("id" )
94142
95- if self .on_response is not None and self .on_response_id == message_id :
143+ if self .on_response is not None \
144+ and self .on_response_id == message ["id" ]:
96145 self .on_response .set_result (message )
97- return
98-
99- params = message .get ("params" )
100- error = message .get ("error" )
101-
102- if error is not None :
103- if error .get ("code" ) == DAHUA_REALM_DHIP :
104- self .sessionId = message .get ("session" )
105- self .send ({"method" : "global.login" , "params" : {"clientType" : "" , "ipAddr" : "(null)" ,
106- "loginType" : "Direct" , "userName" : self .username ,
107- "password" : hashlib .md5 ("{}:{}:{}" .format (self .username , params .get ("random" ),
108- hashlib .md5 ("{}:{}:{}" .format (self .username , params .get ("realm" ), self .password ).encode ("utf-8" )).hexdigest ().upper ()).encode ("utf-8" )).hexdigest ().upper ()}})
109- else :
110- raise Exception ("{}: {}" .format (error .get ("code" ), error .get ("message" )))
111- elif message_id == 2 :
112- self .keepAliveInterval = params .get ("keepAliveInterval" )
113- if self .keepAliveInterval is None or self .heartbeat is not None :
114- raise Exception ("No keepAliveInterval or heartbeat already run" )
115- self .heartbeat = self .loop .create_task (self .heartbeat_loop ())
116- self .send ({"method" : "eventManager.attach" , "params" : {"codes" : ["All" ]}})
117- elif message .get ("method" ) == "client.notifyEventStream" :
118- for message in params .get ("eventList" ):
119- message ["name" ] = self .name
120- self .hass .bus .fire (DOMAIN , message )
146+ else :
147+ self .receive (message )
121148 except Exception as e :
122149 self .on_connection_lost .set_exception (e )
123150
124- def connection_lost (self , exc ):
125- if self .heartbeat is not None :
126- self .heartbeat .cancel ()
127- self .heartbeat = None
128- if not self .on_connection_lost .done ():
129- self .on_connection_lost .set_result (True )
130-
131151 def send (self , message ):
132152 self .request_id += 1
133153 # Removed: "magic": DAHUA_MAGIC ("0x1234")
134154 message ["id" ] = self .request_id
135155 message ["session" ] = self .sessionId
136156 data = json .dumps (message , separators = (',' , ':' ))
137157 _LOGGER .debug (f">>> { data } " )
138- self .transport .write (struct .pack (DAHUA_HEADER_FORMAT , DAHUA_PROTO_DHIP , self .sessionId , self .request_id ,
139- len (data ), len (data )) + data .encode ("utf-8" , "ignore" ))
158+ self .transport .write (
159+ struct .pack (DAHUA_HEADER_FORMAT , DAHUA_PROTO_DHIP ,
160+ self .sessionId , self .request_id , len (data ), len (data ))
161+ + data .encode ("utf-8" , "ignore" ))
140162 return self .request_id
141163
142164 async def command (self , message ):
@@ -148,30 +170,31 @@ async def command(self, message):
148170 self .on_response = self .on_response_id = None
149171
150172 async def open_door (self , channel , short_number ):
151- object_id = (await self .command ({"method" : "accessControl.factory.instance" ,
152- "params" : {"channel" : channel }})).get ("result" )
153- if object_id :
173+ object_id = await self .command ({
174+ "method" : "accessControl.factory.instance" ,
175+ "params" : {"channel" : channel }})
176+ if object_id .get ("result" ):
154177 try :
155- await self .command ({"method" : "accessControl.openDoor" , "object" : object_id ,
178+ await self .command ({
179+ "method" : "accessControl.openDoor" , "object" : object_id ,
156180 "params" : {"DoorIndex" : 0 , "ShortNumber" : short_number }})
157181 finally :
158- await self .command ({"method" : "accessControl.destroy" , "object" : object_id })
159- # Examples:
160- # {"method": "accessControl.getDoorStatus", "object": object_id, "params": {"DoorState": 1}}
161- # {"method": "magicBox.getSystemInfo"}
162- # {"method": "system.methodHelp", "params": {"methodName": methodName}}
163- # {"method": "system.methodSignature", "params": {"methodName": methodName}}
164- # {"method": "system.listService"}
182+ await self .command ({
183+ "method" : "accessControl.destroy" , "object" : object_id })
165184
166185 async def heartbeat_loop (self ):
167186 result = await self .command ({"method" : "magicBox.getSystemInfo" })
168187 if result .get ("result" ):
169188 params = result .get ("params" )
170- self .attrs = {"deviceType" : params .get ("deviceType" ), "serialNumber" : params .get ("serialNumber" )}
189+ self .attrs = {"deviceType" : params .get ("deviceType" ),
190+ "serialNumber" : params .get ("serialNumber" )}
171191 while True :
172192 try :
173193 await asyncio .sleep (self .keepAliveInterval )
174- await self .command ({"method" : "global.keepAlive" , "params" : {"timeout" : self .keepAliveInterval , "action" : True }})
194+ await self .command ({
195+ "method" : "global.keepAlive" ,
196+ "params" : {"timeout" : self .keepAliveInterval ,
197+ "action" : True }})
175198 except asyncio .CancelledError :
176199 raise
177200 except Exception :
@@ -196,24 +219,28 @@ def __init__(self, hass, name, config):
196219 async def async_run (self ):
197220 while True :
198221 try :
199- _LOGGER .debug (f"Connecting { self .config [CONF_HOST ]} :{ self .config [CONF_PORT ]} , username { self .config [CONF_USERNAME ]} " )
222+ _LOGGER .debug ("Connecting {}:{}, username {}" .format (
223+ self .config [CONF_HOST ], self .config [CONF_PORT ],
224+ self .config [CONF_USERNAME ]))
200225 on_connection_lost = self .hass .loop .create_future ()
201- transport , self .protocol = await self .hass .loop .create_connection (lambda : DahuaVTOClient (self .hass ,
202- self ._name , self .config [CONF_USERNAME ], self .config [CONF_PASSWORD ], on_connection_lost ),
226+ t , self .protocol = await self .hass .loop .create_connection (
227+ lambda : DahuaVTOClient (
228+ self .hass , self ._name , self .config [CONF_USERNAME ],
229+ self .config [CONF_PASSWORD ], on_connection_lost ),
203230 self .config [CONF_HOST ], self .config [CONF_PORT ])
204231 try :
205232 await on_connection_lost
233+ raise Exception ("Connection closed" )
206234 finally :
207235 self .protocol = None
208- transport .close ()
236+ t .close ()
209237 await asyncio .sleep (1 )
210- _LOGGER .error (f"{ self .name } : Reconnect" )
211- await asyncio .sleep (5 )
212238 except asyncio .CancelledError :
213239 raise
214240 except Exception as e :
215- _LOGGER .error (f"{ self .name } : { e } " )
216- await asyncio .sleep (30 )
241+ _LOGGER .error ("{}: {}, retry in {} seconds" .format (
242+ self .name , e , self .config [CONF_TIMEOUT ]))
243+ await asyncio .sleep (self .config [CONF_TIMEOUT ])
217244
218245 @property
219246 def should_poll (self ) -> bool :
0 commit comments