Skip to content

Commit 3db139e

Browse files
authored
Merge pull request #21 from roobre/retry
Implement async setup, handle reconnections
2 parents ef88bf0 + a28de7d commit 3db139e

File tree

3 files changed

+109
-57
lines changed

3 files changed

+109
-57
lines changed

__init__.py

Lines changed: 72 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424

2525
UPDATE_TOPIC = f"{DOMAIN}_update"
2626
ERROR_ITERVAL_MAPPING = [0, 10, 60, 300, 600, 3000, 6000]
27+
ERROR_RECONNECT_INTERVAL = 120
2728
NOTIFICATION_ID = "ph803w_device_notification"
2829
NOTIFICATION_TITLE = "PH-803W Device status"
2930

@@ -40,23 +41,12 @@
4041
)
4142

4243

43-
def setup(hass: HomeAssistant, base_config: ConfigType) -> bool:
44+
async def async_setup(hass: HomeAssistant, base_config: ConfigType) -> bool:
4445
"""Set up waterfurnace platform."""
4546

4647
config = base_config[DOMAIN]
4748

48-
host = config[CONF_HOST]
49-
50-
device_client = device.Device(host)
51-
try:
52-
if not device_client.run(once=True):
53-
_LOGGER.error("Device found but no measuremetn was received")
54-
return False
55-
except TimeoutError:
56-
_LOGGER.error("Could no connect ot device")
57-
return False
58-
59-
hass.data[DOMAIN] = DeviceData(hass, device_client)
49+
hass.data[DOMAIN] = DeviceData(hass, config)
6050
hass.data[DOMAIN].start()
6151

6252
discovery.load_platform(hass, Platform.SENSOR, DOMAIN, {}, config)
@@ -72,17 +62,33 @@ class DeviceData(threading.Thread):
7262
for every new data, could work for the pH and ORP data but for the
7363
switches a more direct feedback is wanted."""
7464

75-
def __init__(self, hass, device_client: device.Device) -> None:
65+
def __init__(self, hass, config) -> None:
7666
super().__init__()
7767
self.name = "Ph803wThread"
7868
self.hass = hass
79-
self.device_client = device_client
80-
self.device_client.register_callback(self.dispatcher_new_data)
81-
self.device_client.register_callback(self.reset_fail_counter)
82-
self.host = self.device_client.host
69+
self.host = config[CONF_HOST]
70+
self.device_client = None
8371
self._shutdown = False
8472
self._fails = 0
8573

74+
def connected(self):
75+
return self.device_client is not None
76+
77+
def passcode(self):
78+
if self.device_client is not None:
79+
return self.device_client.passcode
80+
return None
81+
82+
def unique_name(self):
83+
if self.device_client is not None:
84+
return self.device_client.get_unique_name()
85+
return None
86+
87+
def measurement(self):
88+
if self.device_client is not None:
89+
return self.device_client.get_latest_measurement()
90+
return None
91+
8692
def run(self):
8793
"""Thread run loop."""
8894

@@ -92,9 +98,10 @@ def register():
9298

9399
def shutdown(event):
94100
"""Shutdown the thread."""
95-
_LOGGER.debug("Signaled to shutdown")
101+
_LOGGER.info("Signaled to shutdown")
96102
self._shutdown = True
97-
self.device_client.abort()
103+
if self.device_client is not None:
104+
self.device_client.abort()
98105
self.join()
99106

100107
self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, shutdown)
@@ -108,25 +115,53 @@ def shutdown(event):
108115
# least every 4 seconds the device side closes the
109116
# connection.
110117
while True:
111-
if self._shutdown:
112-
_LOGGER.debug("Graceful shutdown")
113-
return
118+
self.device_client = None
119+
120+
_LOGGER.info(f"Attempting to connect to device at {self.host}")
121+
device_client = device.Device(self.host)
114122

115123
try:
116-
self.device_client.run(once=False)
117-
except (device.DeviceError, RecursionError, ConnectionError):
118-
_LOGGER.exception("Failed to read data, attempting to recover")
119-
self.device_client.close()
120-
self._fails += 1
121-
error_mapping = self._fails
122-
if error_mapping >= len(ERROR_ITERVAL_MAPPING):
123-
error_mapping = len(ERROR_ITERVAL_MAPPING) - 1
124-
sleep_time = ERROR_ITERVAL_MAPPING[error_mapping]
125-
_LOGGER.debug(
126-
"Sleeping for fail #%s, in %s seconds", self._fails, sleep_time
127-
)
128-
self.device_client.reset_socket()
129-
time.sleep(sleep_time)
124+
if not device_client.run(once=True):
125+
_LOGGER.info(
126+
f"Device found but no measurement was received, reconnecting in {ERROR_RECONNECT_INTERVAL} seconds")
127+
time.sleep(ERROR_RECONNECT_INTERVAL)
128+
continue
129+
130+
except Exception as e:
131+
_LOGGER.info(
132+
f"Error connecting to device at {self.host}: {str(e)}")
133+
_LOGGER.info(
134+
f"Retrying connection in {ERROR_RECONNECT_INTERVAL} seconds")
135+
time.sleep(ERROR_RECONNECT_INTERVAL)
136+
continue
137+
138+
self.device_client = device_client
139+
_LOGGER.debug("Registering callbacks")
140+
self.device_client.register_callback(self.dispatcher_new_data)
141+
self.device_client.register_callback(self.reset_fail_counter)
142+
143+
_LOGGER.info(f"Connected to {self.host}")
144+
145+
while True:
146+
if self._shutdown:
147+
_LOGGER.debug("Graceful shutdown")
148+
return
149+
150+
try:
151+
_LOGGER.info("Starting device client loop")
152+
self.device_client.run(once=False)
153+
except Exception as e:
154+
_LOGGER.exception(f"Failed to read data: {str(e)}")
155+
self.device_client.close()
156+
self._fails += 1
157+
error_mapping = self._fails
158+
if error_mapping >= len(ERROR_ITERVAL_MAPPING):
159+
error_mapping = len(ERROR_ITERVAL_MAPPING) - 1
160+
sleep_time = ERROR_ITERVAL_MAPPING[error_mapping]
161+
_LOGGER.info(
162+
f"Sleeping {str(sleep_time)}s for failure #{str(self._fails)}")
163+
self.device_client.reset_socket()
164+
time.sleep(sleep_time)
130165

131166
@callback
132167
def reset_fail_counter(self):

binary_sensor.py

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from homeassistant.helpers.entity_platform import AddEntitiesCallback
1313
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
1414
from homeassistant.util import slugify
15+
from homeassistant.exceptions import PlatformNotReady
1516

1617
from . import UPDATE_TOPIC
1718
from .const import DOMAIN
@@ -63,10 +64,10 @@ def __init__(
6364
]
6465

6566

66-
def setup_platform(
67+
async def async_setup_platform(
6768
hass: HomeAssistant,
6869
config: ConfigType,
69-
add_entities: AddEntitiesCallback,
70+
async_add_entities: AddEntitiesCallback,
7071
discovery_info: DiscoveryInfoType | None = None,
7172
) -> None:
7273
"""Set up the PH-803W sensor."""
@@ -75,10 +76,14 @@ def setup_platform(
7576

7677
sensors = []
7778
device_data = hass.data[DOMAIN]
79+
if not device_data.connected():
80+
raise PlatformNotReady(f"PH-803W not connected yet")
81+
82+
_LOGGER.info(f"PH-803W connected, creating entities")
7883
for sconfig in SENSORS:
7984
sensors.append(DeviceSensor(device_data, sconfig))
8085

81-
add_entities(sensors)
86+
async_add_entities(sensors)
8287

8388

8489
class DeviceSensor(BinarySensorEntity):
@@ -90,12 +95,15 @@ def __init__(self, device_data, config):
9095
self._name = config.friendly_name
9196
self._attr = config.field
9297
self._state = None
93-
if self.device_data.device_client.get_latest_measurement() is not None:
98+
99+
measurement = self.device_data.measurement()
100+
if measurement is not None:
94101
self._state = getattr(
95-
self.device_data.device_client.get_latest_measurement(),
102+
measurement,
96103
self._attr,
97104
None,
98105
)
106+
99107
self._icon = config.icon
100108
self._unit_of_measurement = config.unit_of_measurement
101109
self._attr_device_class = config.device_class
@@ -114,14 +122,14 @@ def name(self):
114122
def device_info(self):
115123
"""Return information to link this entity with the correct device."""
116124
return {
117-
"identifiers": {(DOMAIN, self.device_data.device_client.passcode)},
118-
"name": self.device_data.device_client.get_unique_name(),
125+
"identifiers": {(DOMAIN, self.device_data.passcode())},
126+
"name": self.device_data.unique_name(),
119127
}
120128

121129
@property
122130
def unique_id(self):
123131
"""Return the sensor unique id."""
124-
return self.device_data.device_client.passcode + self._attr
132+
return self.device_data.passcode() + self._attr
125133

126134
@property
127135
def is_on(self):
@@ -154,9 +162,10 @@ async def async_added_to_hass(self):
154162
@callback
155163
def async_update_callback(self):
156164
"""Update state."""
157-
if self.device_data.device_client.get_latest_measurement() is not None:
165+
measurement = self.device_data.measurement()
166+
if measurement is not None:
158167
self._state = getattr(
159-
self.device_data.device_client.get_latest_measurement(),
168+
measurement,
160169
self._attr,
161170
None,
162171
)

sensor.py

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from homeassistant.helpers.entity_platform import AddEntitiesCallback
1616
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
1717
from homeassistant.util import slugify
18+
from homeassistant.exceptions import PlatformNotReady
1819

1920
from . import UPDATE_TOPIC
2021
from .const import DOMAIN
@@ -53,10 +54,10 @@ def __init__(
5354
]
5455

5556

56-
def setup_platform(
57+
async def async_setup_platform(
5758
hass: HomeAssistant,
5859
config: ConfigType,
59-
add_entities: AddEntitiesCallback,
60+
async_add_entities: AddEntitiesCallback,
6061
discovery_info: DiscoveryInfoType | None = None,
6162
) -> None:
6263
"""Set up the PH-803W sensor."""
@@ -65,10 +66,14 @@ def setup_platform(
6566

6667
sensors = []
6768
device_data = hass.data[DOMAIN]
69+
if not device_data.connected():
70+
raise PlatformNotReady(f"PH-803W not connected yet")
71+
72+
_LOGGER.info(f"PH-803W connected, creating entities")
6873
for sconfig in SENSORS:
6974
sensors.append(DeviceSensor(device_data, sconfig))
7075

71-
add_entities(sensors)
76+
async_add_entities(sensors)
7277

7378

7479
class DeviceSensor(SensorEntity):
@@ -80,12 +85,15 @@ def __init__(self, device_data, config):
8085
self._name = config.friendly_name
8186
self._attr = config.field
8287
self._state = None
83-
if self.device_data.device_client.get_latest_measurement() is not None:
88+
89+
measurement = self.device_data.measurement()
90+
if measurement is not None:
8491
self._state = getattr(
85-
self.device_data.device_client.get_latest_measurement(),
92+
measurement,
8693
self._attr,
8794
None,
8895
)
96+
8997
self._icon = config.icon
9098
self._unit_of_measurement = config.unit_of_measurement
9199
self._attr_device_class = config.device_class
@@ -104,14 +112,14 @@ def name(self):
104112
def device_info(self):
105113
"""Return information to link this entity with the correct device."""
106114
return {
107-
"identifiers": {(DOMAIN, self.device_data.device_client.passcode)},
108-
"name": self.device_data.device_client.get_unique_name(),
115+
"identifiers": {(DOMAIN, self.device_data.passcode())},
116+
"name": self.device_data.unique_name(),
109117
}
110118

111119
@property
112120
def unique_id(self):
113121
"""Return the sensor unique id."""
114-
return self.device_data.device_client.passcode + self._attr
122+
return self.device_data.passcode() + self._attr
115123

116124
@property
117125
def native_value(self):
@@ -144,9 +152,9 @@ async def async_added_to_hass(self):
144152
@callback
145153
def async_update_callback(self):
146154
"""Update state."""
147-
if self.device_data.device_client.get_latest_measurement() is not None:
155+
if self.device_data.measurement() is not None:
148156
self._state = getattr(
149-
self.device_data.device_client.get_latest_measurement(),
157+
self.device_data.measurement(),
150158
self._attr,
151159
None,
152160
)

0 commit comments

Comments
 (0)