Skip to content

Commit d9ded19

Browse files
committed
PEP8 fixes, configurable retry timeout
1 parent 4f3ef7e commit d9ded19

File tree

3 files changed

+100
-66
lines changed

3 files changed

+100
-66
lines changed

README.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
[![Validate](https://github.com/myhomeiot/DahuaVTO/workflows/Validate/badge.svg)](https://github.com/myhomeiot/DahuaVTO/actions)
2+
13
A Home Assistant custom integration for control Dahua VTO/VTH devices.
24

35
The following models are reported as working:
@@ -44,10 +46,11 @@ sensor:
4446
- platform: dahua_vto
4547
name: NAME_HERE
4648
host: HOST_HERE
47-
port: PORT_HERE optional, default is 5000
49+
timeout: TIMEOUT_HERE optional, default 10
50+
port: PORT_HERE optional, default 5000
4851
username: USERNAME_HERE_OR_secrets.yaml
4952
password: PASSWORD_HERE_OR_secrets.yaml
50-
scan_interval: INTERVAL_HERE optional, default 60
53+
scan_interval: SCAN_INTERVAL_HERE optional, default 60
5154
```
5255
5356
Example:

custom_components/dahua_vto/__init__.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@
1919
{
2020
vol.Required(CONF_ENTITY_ID): cv.string,
2121
vol.Required(CONF_CHANNEL): int,
22-
vol.Optional(CONF_SHORT_NUMBER, default=DEFAULT_SHORT_NUMBER): cv.string,
22+
vol.Optional(CONF_SHORT_NUMBER,
23+
default=DEFAULT_SHORT_NUMBER): cv.string,
2324
}
2425
)
2526

@@ -32,14 +33,17 @@ async def service_open_door(event):
3233
if entity.protocol is None:
3334
raise HomeAssistantError("not connected")
3435
try:
35-
return await entity.protocol.open_door(event.data[CONF_CHANNEL] - 1, event.data[CONF_SHORT_NUMBER])
36+
return await entity.protocol.open_door(
37+
event.data[CONF_CHANNEL] - 1,
38+
event.data[CONF_SHORT_NUMBER])
3639
except asyncio.TimeoutError:
3740
raise HomeAssistantError("timeout")
3841
else:
3942
raise HomeAssistantError("entity not found")
4043

4144
hass.data.setdefault(DOMAIN, {})
4245
hass.helpers.service.async_register_admin_service(
43-
DOMAIN, SERVICE_OPEN_DOOR, service_open_door, schema=SERVICE_OPEN_DOOR_SCHEMA
46+
DOMAIN, SERVICE_OPEN_DOOR, service_open_door,
47+
schema=SERVICE_OPEN_DOOR_SCHEMA
4448
)
4549
return True

custom_components/dahua_vto/sensor.py

Lines changed: 88 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,19 @@
99
import homeassistant.helpers.config_validation as cv
1010
import voluptuous as vol
1111
from 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

1415
DOMAIN = "dahua_vto"
1516
DAHUA_PROTO_DHIP = 0x5049484400000020
1617
DAHUA_HEADER_FORMAT = "<QLLQQ"
1718
DAHUA_REALM_DHIP = 268632079 # DHIP REALM Login Challenge
19+
DAHUA_LOGIN_PARAMS = {
20+
"clientType": "", "ipAddr": "(null)", "loginType": "Direct"}
1821

1922
DEFAULT_NAME = "Dahua VTO"
2023
DEFAULT_PORT = 5000
24+
DEFAULT_TIMEOUT = 10
2125

2226
_LOGGER = logging.getLogger(__name__)
2327

@@ -26,12 +30,15 @@
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

Comments
 (0)