Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions examples/door_window_sensor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import asyncio
import os
from typing import List

from meross_iot.controller.subdevice import Ms200Sensor
from meross_iot.http_api import MerossHttpClient
from meross_iot.manager import MerossManager
from meross_iot.model.enums import Namespace, OnlineStatus

EMAIL = os.environ.get('MEROSS_EMAIL') or "YOUR_MEROSS_CLOUD_EMAIL"
PASSWORD = os.environ.get('MEROSS_PASSWORD') or "YOUR_MEROSS_CLOUD_PASSWORD"
API_URL = "https://iot.meross.com"

async def opening_event(namespace: Namespace, data: dict, device_internal_id: str, *args, **kwargs):
print("An event has occurred!")
if namespace == Namespace.CONTROL_ALARM:
print(f"Alarm occurred! Event data: {data}")
elif namespace == Namespace.HUB_SENSOR_DOORWINDOW:
print(f"opening occurred! Event data: {data}")
else:
print(f"Another event occurred: {namespace.value}, Event data: {data}")


async def main():
# Setup the HTTP client API from user-password
http_api_client = await MerossHttpClient.async_from_user_password(email=EMAIL, password=PASSWORD, api_base_url=API_URL)

# Setup and start the device manager
manager = MerossManager(http_client=http_api_client)
await manager.async_init()

# Retrieve all the MS200 devices that are registered on this account
await manager.async_device_discovery()

# Retrieve door/window sensors : ms200
door_window_sensors: List[Ms200Sensor] = manager.find_devices(device_class=Ms200Sensor, online_status=OnlineStatus.ONLINE)

if len(door_window_sensors) < 1:
print("No online door window sensors found!")
else:
# Let's register an event handle to quickly react in case of opening
for sensor in door_window_sensors:
sensor.register_push_notification_handler_coroutine(opening_event)

# Manually force and update to retrieve the latest event from
# the device. This ensures we get the most recent data and not a cached value
while True:
try:
for sensor in door_window_sensors:
print(f"Sensor {sensor.name} - Current open status = {sensor.is_opened}. "
f"Is currently opened? {sensor.is_opened}. "
f"Last timestamp of opening = {sensor.latest_detected_opening_ts if sensor.latest_detected_opening_ts is not None else 'NEVER'}")
print("Press CTRL+C to terminate.")
# Let's wait a bit for some events to occur
await asyncio.sleep(10)
except InterruptedError as e:
print("Execution terminated by the user")

# Close the manager and logout from http_api
manager.close()
await http_api_client.async_logout()

if __name__ == '__main__':
if os.name == 'nt':
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
loop.stop()
1 change: 1 addition & 0 deletions meross_iot/controller/mixins/hub.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ class HubMixn(object):
Namespace.HUB_TOGGLEX: 'togglex',
Namespace.HUB_BATTERY: 'battery',
Namespace.HUB_SENSOR_WATERLEAK: 'waterLeak',
Namespace.HUB_SENSOR_DOORWINDOW: 'doorWindow',
}

def __init__(self, device_uuid: str,
Expand Down
141 changes: 140 additions & 1 deletion meross_iot/controller/subdevice.py
Original file line number Diff line number Diff line change
Expand Up @@ -597,4 +597,143 @@ async def async_handle_subdevice_notification(self, namespace: Namespace, data:
return locally_handled or parent_handled

def __repr__(self) -> str:
return f"<Ms400Device(uuid={self.uuid}, is_leaking={self.is_leaking})>"
return f"<Ms400Device(uuid={self.uuid}, is_leaking={self.is_leaking})>"

class Ms200Sensor(GenericSubDevice):
"""
Class that represents a Meross MS200 Door Window Sensor
"""

def __init__(self, hubdevice_uuid: str, subdevice_id: str, manager, max_events_queue_len=30, **kwargs):
super().__init__(hubdevice_uuid, subdevice_id, manager, **kwargs)

self._last_active_time: Optional[int] = None
# Represents the last time we contacted the device

self.__state: Optional[bool] = None
# Represents the current state

self.__last_event_ts: Optional[int] = None
# Represents the timestamp of the last sample (current state sampling)

self.__cached_events: deque = deque(maxlen=max_events_queue_len)
# Last N samples we collected

self.__last_opened_event_ts: Optional[int] = None
# Last timestamp we've seen an opening

@property
def is_opened(self) -> Optional[bool]:
"""
Returns the latest updated state available , if available.
"""
return self.__state

@property
def latest_sample_time(self) -> Optional[int]:
"""
Returns the timestamp (GMT) of the latest available sampling.
"""
return self.__last_event_ts

@property
def latest_detected_opening_ts(self) -> Optional[int]:
"""
Return the timestamp (GMT) of the latest time the sensor sampled an opening
"""
return self.__last_opened_event_ts

@property
def get_last_events(self) -> List[Dict]:
"""
Returns the last cached items
"""
return [x for x in self.__cached_events]

async def async_update(self,
timeout: Optional[float] = None,
*args,
**kwargs) -> None:
# Make sure we issue an update at HUB level first
await super().async_update()

# We also need to trigger an update request for this specific sub-device
result = await self._hub._execute_command(method="GET",
namespace=Namespace.HUB_SENSOR_ALL,
payload={'all': [{'id': self.subdevice_id}]},
timeout=timeout)

# Retrieve the sub-device specific data and update the status
subdevices_states = result.get('all')
for subdev_state in subdevices_states:
subdev_id = subdev_state.get('id')
if subdev_id != self.subdevice_id:
continue
await self.async_handle_subdevice_notification(namespace=Namespace.HUB_SENSOR_ALL, data=subdev_state)
break

def _handle_opening_fresh_data(self, open: bool, timestamp: int):
# If handling an event with an older timestamp than the one we have, just discard it.
if self.latest_sample_time is not None and timestamp <= self.latest_sample_time:
return

# If this is the first update or if it's more recent than the last we have, update the current state.
if self.__last_event_ts is None or timestamp >= self.__last_event_ts:
self.__last_event_ts = timestamp
self.__state = open

# If the event is a leak and is more recent than the latest leak event, update it.
if open and (self.__last_opened_event_ts is None or timestamp >= self.__last_opened_event_ts):
self.__last_opened_event_ts = timestamp

# In any case, register the event in the queue
self.__cached_events.append({
"status": open,
"lmTime": timestamp
})

async def async_handle_push_notification(self, namespace: Namespace, data: dict) -> bool:
locally_handled = False
if namespace == Namespace.HUB_ONLINE:
update_element = self._prepare_push_notification_data(data=data, filter_accessor='online')
if update_element is not None:
self._online = OnlineStatus(update_element.get('status', -1))
locally_handled = True
elif namespace == Namespace.HUB_SENSOR_DOORWINDOW:
opening = data.get('doorWindow')
latestOpening = opening.get('status')
latestOpeningTime = opening.get('lmTime')
self._handle_opening_fresh_data(open=latestOpening==1, timestamp=latestOpeningTime)
locally_handled = True

return locally_handled

async def async_handle_subdevice_notification(self, namespace: Namespace, data: dict) -> bool:
locally_handled = False
if namespace == Namespace.HUB_ONLINE:
self._online = OnlineStatus(data.get('online', {}).get('status', -1))
self._last_active_time = data.get('online', {}).get('lastActiveTime')
elif namespace == Namespace.HUB_SENSOR_DOORWINDOW:
latestOpening = data.get('status')
latestOpeningTime = data.get('lmTime')
self._handle_opening_fresh_data(open=latestOpening==1, timestamp=latestOpeningTime)
locally_handled = True
elif namespace == Namespace.HUB_SENSOR_ALL:
self._online = OnlineStatus(data.get('online', {}).get('status', -1))
opening = data.get('doorWindow')
if opening is not None:
latestOpening = opening.get('status')
latestOpeningTime = opening.get('lmTime')
self._handle_opening_fresh_data(open=latestOpening == 1, timestamp=latestOpeningTime)

locally_handled = True
else:
_LOGGER.warning(f"Could not handle event %s in subdevice %s handler", namespace, self.name)

# Always call the parent handler when done with local specific logic. This gives the opportunity to all
# ancestors to catch all events.
parent_handled = await super().async_handle_push_notification(namespace=namespace, data=data)
return locally_handled or parent_handled

def __repr__(self) -> str:
return f"<Ms200Device(uuid={self.uuid}, is_opened={self.is_opened})>"
8 changes: 5 additions & 3 deletions meross_iot/device_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
from meross_iot.controller.mixins.system import SystemAllMixin, SystemOnlineMixin
from meross_iot.controller.mixins.thermostat import ThermostatModeMixin, ThermostatModeBMixin
from meross_iot.controller.mixins.toggle import ToggleXMixin, ToggleMixin
from meross_iot.controller.subdevice import Mts100v3Valve, Ms100Sensor, Ms405Sensor
from meross_iot.controller.subdevice import Mts100v3Valve, Ms100Sensor, Ms405Sensor,Ms200Sensor
from meross_iot.model.enums import Namespace
from meross_iot.model.exception import UnknownDeviceType
from meross_iot.model.http.device import HttpDeviceInfo
Expand All @@ -30,7 +30,8 @@
"mts100v3": Mts100v3Valve,
"ms100": Ms100Sensor,
"ms100f": Ms100Sensor,
"ms405": Ms405Sensor
"ms405": Ms405Sensor,
"ms200" : Ms200Sensor
}

_ABILITY_MATRIX = {
Expand Down Expand Up @@ -93,7 +94,8 @@
"ms100": Ms100Sensor,
"ms100f": Ms100Sensor,
"ms405": Ms405Sensor,
"ms400": Ms405Sensor
"ms400": Ms405Sensor,
"ms200": Ms200Sensor,
}

_dynamic_types = {}
Expand Down
1 change: 1 addition & 0 deletions meross_iot/model/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ class Namespace(Enum):
HUB_SENSOR_TEMPHUM = 'Appliance.Hub.Sensor.TempHum'
HUB_SENSOR_ALERT = 'Appliance.Hub.Sensor.Alert'
HUB_SENSOR_WATERLEAK = 'Appliance.Hub.Sensor.WaterLeak'
HUB_SENSOR_DOORWINDOW = 'Appliance.Hub.Sensor.DoorWindow'

# MTS100
HUB_MTS100_ALL = 'Appliance.Hub.Mts100.All'
Expand Down
67 changes: 67 additions & 0 deletions meross_iot/model/push/door_window.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import logging
from typing import Optional, Any, Tuple

from meross_iot.model.enums import Namespace, OnlineStatus
from meross_iot.model.push.generic import GenericPushNotification

_LOGGER = logging.getLogger(__name__)


class DoorWindowPushNotification(GenericPushNotification):
def __init__(self, originating_device_uuid: str, raw_data: dict):
super().__init__(namespace=Namespace.HUB_SENSOR_DOORWINDOW,
originating_device_uuid=originating_device_uuid,
raw_data=raw_data)
opening = raw_data.get('doorWindow')
if len(opening) != 1:
_LOGGER.error("Triggered event with #%d events. This library can only handle alarm with a single trigger. Please open a BUG and reporto this payload: %s", len(opening), str(raw_data))

# We assume we'll always have at least 1 alarm event
event = opening[0]
self._sub_device_id = event.get("id")
self._latest_opening = event.get("status")
self._latest_sample_time = event.get("lmTime")
self._synced_time = event.get("syncedTime")
self._samples = event.get("sample")

@property
def syncedTime(self) -> Optional[Tuple[int, int]]:
"""
Returns the last samples for water-leak.
:return:
"""
if self._synced_time is None:
return None
else:
return self._synced_time

@property
def latestSampleTime(self) -> Optional[int]:
"""
Returns the latest sample timestamp
:return:
"""
if self._latest_sample_time is None:
return None
else:
return self._latest_sample_time

@property
def latestSampleIsOpened(self) -> Optional[int]:
"""
Returns true if the last sampling was an opening, false otherwise
:return:
"""
if self._latest_opening is None:
return None
else:
return self._latest_opening

@property
def subdevice_id(self) -> Optional[str]:
"""
If this event refers to a sub-device, this property contains the sub-device ID.
In all other cases, this is None.
:return:
"""
return self._sub_device_id
2 changes: 2 additions & 0 deletions meross_iot/model/push/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from meross_iot.model.push.online import OnlinePushNotification
from meross_iot.model.push.unbind import UnbindPushNotification
from meross_iot.model.push.water_leak import WaterLeakPushNotification
from meross_iot.model.push.door_window import DoorWindowPushNotification

_LOGGER = logging.getLogger(__name__)

Expand All @@ -17,6 +18,7 @@
Namespace.SYSTEM_ONLINE: OnlinePushNotification,
Namespace.CONTROL_ALARM: AlarmPushNotification,
Namespace.HUB_SENSOR_WATERLEAK: WaterLeakPushNotification,
Namespace.HUB_SENSOR_DOORWINDOW: DoorWindowPushNotification,
}


Expand Down