Skip to content

Commit 98272d5

Browse files
Improved water leak sensor device, added sample and version bump
1 parent 8fbd5f8 commit 98272d5

4 files changed

Lines changed: 113 additions & 38 deletions

File tree

.version

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.4.10.3alpha3
1+
0.4.10.3

README.md

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ Due to the popularity of the library, I've decided to list it publicly on the Pi
3232
So, the installation is as simple as typing the following command:
3333

3434
```bash
35-
pip install meross_iot==0.4.10.2
35+
pip install meross_iot==0.4.10.3
3636
```
3737

3838
## Usage & Full Documentation
@@ -124,6 +124,7 @@ The list of tested devices is the following:
124124
- MS100F (temperature/humidity sensor)
125125
- MSS710
126126
- MSXH0 (Smart Humidifier)
127+
- MS400 and MS405 (Water leak sensors)
127128

128129
I'd like to thank all the people who contributed to the early stage of library development,
129130
who stimulated me to continue the development and making this library support more devices.
@@ -199,14 +200,17 @@ Anyway, feel free to contribute via donations!
199200
</p>
200201

201202
## Changelog
202-
### 0.4.10.2
203-
- Allows to pass a specific ssl context to the manager to perform MQTT ssl connections using that SSL context.
204-
This is a breaking change: signature of MerossManager() has changed.
205-
203+
### 0.4.10.3
204+
- Add support for water leak sensors such as MS400 and MS405
205+
- Add water-leak sensor examples
206206

207207
<details>
208208
<summary>Older</summary>
209209

210+
### 0.4.10.2
211+
- Allows to pass a specific ssl context to the manager to perform MQTT ssl connections using that SSL context.
212+
This is a breaking change: signature of MerossManager() has changed.
213+
210214
### 0.4.9.2
211215
- Partially Address #569, fix error when dealing with unknown sub-devices. Water leak sensor not supported yet.
212216

examples/water_leak_sensor.py

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,26 @@
11
import asyncio
22
import os
3+
from typing import List
34

5+
from meross_iot.controller.subdevice import Ms405Sensor
46
from meross_iot.http_api import MerossHttpClient
57
from meross_iot.manager import MerossManager
8+
from meross_iot.model.enums import Namespace, OnlineStatus
69

710
EMAIL = os.environ.get('MEROSS_EMAIL') or "YOUR_MEROSS_CLOUD_EMAIL"
811
PASSWORD = os.environ.get('MEROSS_PASSWORD') or "YOUR_MEROSS_CLOUD_PASSWORD"
912

1013

14+
async def water_leak_event(namespace: Namespace, data: dict, device_internal_id: str, *args, **kwargs):
15+
print("An event has occurred!")
16+
if namespace == Namespace.CONTROL_ALARM:
17+
print(f"Alarm occurred! Event data: {data}")
18+
elif namespace == Namespace.HUB_SENSOR_WATERLEAK:
19+
print(f"Water leak occurred! Event data: {data}")
20+
else:
21+
print(f"Another event occurred: {namespace.value}, Event data: {data}")
22+
23+
1124
async def main():
1225
# Setup the HTTP client API from user-password
1326
http_api_client = await MerossHttpClient.async_from_user_password(email=EMAIL, password=PASSWORD, api_base_url="https://iot.meross.com")
@@ -19,26 +32,29 @@ async def main():
1932
# Retrieve all the MS100 devices that are registered on this account
2033
await manager.async_device_discovery()
2134

22-
msh400 = manager.find_devices(device_type="msh400")
23-
hub = msh400[0]
24-
25-
26-
water_leak_sensors = manager.find_devices(device_type="ms405")
35+
# Retrieve water leak sensors. Can either be ms400 or ms405
36+
water_leak_sensors: List[Ms405Sensor] = manager.find_devices(device_class=Ms405Sensor, online_status=OnlineStatus.ONLINE)
2737

2838
if len(water_leak_sensors) < 1:
29-
print("No MSH405 sensor found...")
39+
print("No online water leak sensors found!")
3040
else:
31-
dev = water_leak_sensors[0]
41+
# Let's register an event handle to quickly react in case of water leaks
42+
for sensor in water_leak_sensors:
43+
sensor.register_push_notification_handler_coroutine(water_leak_event)
3244

3345
# Manually force and update to retrieve the latest temperature sensed from
3446
# the device. This ensures we get the most recent data and not a cached value
35-
await dev.async_update()
36-
37-
# Access read cached data
38-
print(f"IS_LEAKING={dev.is_leaking}")
39-
40-
# Let's wait a bit for some events to occur
41-
await asyncio.sleep(3600)
47+
while True:
48+
try:
49+
for sensor in water_leak_sensors:
50+
print(f"Sensor {sensor.name} - Current leak status = {sensor.is_leaking}. "
51+
f"Is currently leaking? {sensor.is_leaking}. "
52+
f"Last timestamp of leak = {sensor.latest_detected_water_leak_ts if sensor.latest_detected_water_leak_ts is not None else 'NEVER'}")
53+
print("Press CTRL+C to terminate.")
54+
# Let's wait a bit for some events to occur
55+
await asyncio.sleep(10)
56+
except InterruptedError as e:
57+
print("Execution terminated by the user")
4258

4359
# Close the manager and logout from http_api
4460
manager.close()

meross_iot/controller/subdevice.py

Lines changed: 73 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import logging
2+
from collections import deque
23
from datetime import datetime
3-
from typing import Optional, Iterable
4+
from typing import Optional, Iterable, List, Dict
45

56
from meross_iot.controller.device import GenericSubDevice
67
from meross_iot.model.enums import OnlineStatus, ThermostatV3Mode
@@ -463,22 +464,51 @@ class Ms405Sensor(GenericSubDevice):
463464
Class that represents a Meross MS400 Smart Water Leak Sensor.
464465
"""
465466

466-
def __init__(self, hubdevice_uuid: str, subdevice_id: str, manager, **kwargs):
467+
def __init__(self, hubdevice_uuid: str, subdevice_id: str, manager, max_events_queue_len=30, **kwargs):
467468
super().__init__(hubdevice_uuid, subdevice_id, manager, **kwargs)
468-
self.__water_leak = {}
469-
self._last_active_time = None
469+
470+
self._last_active_time: Optional[int] = None
471+
# Represents the last time we contacted the device
472+
473+
self.__water_leak_state: Optional[bool] = None
474+
# Represents the current state
475+
476+
self.__last_event_ts: Optional[int] = None
477+
# Represents the timestamp of the last sample (current state sampling)
478+
479+
self.__cached_events: deque = deque(maxlen=max_events_queue_len)
480+
# Last N samples we collected
481+
482+
self.__last_waterleak_event_ts: Optional[int] = None
483+
# Last timestamp we've seen a leak
470484

471485
@property
472486
def is_leaking(self) -> Optional[bool]:
473487
"""
474-
Returns True if a water leak is detected (1), False otherwise (0).
475-
Returns None if the state has not been fetched yet.
488+
Returns the latest updated state available for the water leak sensor, if available.
476489
"""
477-
cur_val = self.__water_leak.get("latestWaterLeak")
478-
if cur_val is None:
479-
return None
480-
else:
481-
return cur_val == 1
490+
return self.__water_leak_state
491+
492+
@property
493+
def latest_sample_time(self) -> Optional[int]:
494+
"""
495+
Returns the timestamp (GMT) of the latest available sampling.
496+
"""
497+
return self.__last_event_ts
498+
499+
@property
500+
def latest_detected_water_leak_ts(self) -> Optional[int]:
501+
"""
502+
Return the timestamp (GMT) of the latest time the sensor sampled a water leak.
503+
"""
504+
return self.__last_waterleak_event_ts
505+
506+
@property
507+
def get_last_events(self) -> List[Dict]:
508+
"""
509+
Returns the last cached items
510+
"""
511+
return [x for x in self.__cached_events]
482512

483513
async def async_update(self,
484514
timeout: Optional[float] = None,
@@ -503,6 +533,26 @@ async def async_update(self,
503533
await self.async_handle_subdevice_notification(namespace=Namespace.HUB_SENSOR_ALL, data=subdev_state)
504534
break
505535

536+
def _handle_water_leak_fresh_data(self, leaking: bool, timestamp: int):
537+
# If handling an event with an older timestamp than the one we have, just discard it.
538+
if self.latest_sample_time is not None and timestamp <= self.latest_sample_time:
539+
return
540+
541+
# If this is the first update or if it's more recent than the last we have, update the current state.
542+
if self.__last_event_ts is None or timestamp >= self.__last_event_ts:
543+
self.__last_event_ts = timestamp
544+
self.__water_leak_state = leaking
545+
546+
# If the event is a leak and is more recent than the latest leak event, update it.
547+
if leaking and (self.__last_waterleak_event_ts is None or timestamp >= self.__last_waterleak_event_ts):
548+
self.__last_waterleak_event_ts = timestamp
549+
550+
# In any case, register the event in the queue
551+
self.__cached_events.append({
552+
"leaking": leaking,
553+
"timestamp": timestamp
554+
})
555+
506556
async def async_handle_push_notification(self, namespace: Namespace, data: dict) -> bool:
507557
locally_handled = False
508558
if namespace == Namespace.HUB_ONLINE:
@@ -511,7 +561,10 @@ async def async_handle_push_notification(self, namespace: Namespace, data: dict)
511561
self._online = OnlineStatus(update_element.get('status', -1))
512562
locally_handled = True
513563
elif namespace == Namespace.HUB_SENSOR_WATERLEAK:
514-
self.__water_leak = data.get('waterLeak')
564+
water_leak_state = data.get('waterLeak')
565+
latestWaterLeak = water_leak_state.get('latestWaterLeak')
566+
latestSampleTime = water_leak_state.get('latestSampleTime')
567+
self._handle_water_leak_fresh_data(leaking=latestWaterLeak==1, timestamp=latestSampleTime)
515568
locally_handled = True
516569

517570
return locally_handled
@@ -523,15 +576,17 @@ async def async_handle_subdevice_notification(self, namespace: Namespace, data:
523576
self._last_active_time = data.get('online', {}).get('lastActiveTime')
524577
elif namespace == Namespace.HUB_SENSOR_WATERLEAK:
525578
latestWaterLeak = data.get('latestWaterLeak')
526-
latestSaampleTime = data.get('latestSampleTime')
527-
if latestWaterLeak is not None:
528-
self.__water_leak["latestWaterLeak"]=latestWaterLeak
529-
if latestSaampleTime is not None:
530-
self.__water_leak["latestSaampleTime"] = latestSaampleTime
579+
latestSampleTime = data.get('latestSampleTime')
580+
self._handle_water_leak_fresh_data(leaking=latestWaterLeak==1, timestamp=latestSampleTime)
531581
locally_handled = True
532582
elif namespace == Namespace.HUB_SENSOR_ALL:
533583
self._online = OnlineStatus(data.get('online', {}).get('status', -1))
534-
self.__water_leak.update(data.get('waterLeak', {}))
584+
water_leak_state = data.get('waterLeak')
585+
if water_leak_state is not None:
586+
latestWaterLeak = water_leak_state.get('latestWaterLeak')
587+
latestSampleTime = water_leak_state.get('latestSampleTime')
588+
self._handle_water_leak_fresh_data(leaking=latestWaterLeak == 1, timestamp=latestSampleTime)
589+
535590
locally_handled = True
536591
else:
537592
_LOGGER.warning(f"Could not handle event %s in subdevice %s handler", namespace, self.name)

0 commit comments

Comments
 (0)