Skip to content

Commit

Permalink
- General Refactor
Browse files Browse the repository at this point in the history
  • Loading branch information
albertogeniola committed Feb 9, 2020
1 parent 93176b4 commit 238dfd0
Show file tree
Hide file tree
Showing 4 changed files with 148 additions and 118 deletions.
2 changes: 1 addition & 1 deletion custom_components/meross_cloud/climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ def device_event_handler(self, evt):
self._target_temperature = float(evt.temperature.get('currentSet')) / 10
self._heating = evt.temperature.get('heating') == 1
elif isinstance(evt, ThermostatModeChange):
pass
self._device_mode = evt.mode
else:
_LOGGER.warning("Unhandled/ignored event: %s" % str(evt))

Expand Down
91 changes: 44 additions & 47 deletions custom_components/meross_cloud/cover.py
Original file line number Diff line number Diff line change
@@ -1,58 +1,64 @@
import logging

from homeassistant.components.cover import (SUPPORT_CLOSE, SUPPORT_OPEN,
CoverDevice, DEVICE_CLASS_GARAGE)
from homeassistant.const import (STATE_CLOSED, STATE_CLOSING, STATE_OPEN,
STATE_OPENING, STATE_UNKNOWN)
from meross_iot.cloud.devices.door_openers import GenericGarageDoorOpener
from meross_iot.meross_event import MerossEventType

from .common import (DOMAIN, MANAGER)
from meross_iot.meross_event import MerossEventType, DeviceDoorStatusEvent
from .common import (DOMAIN, MANAGER, AbstractMerossEntityWrapper, cloud_io)

_LOGGER = logging.getLogger(__name__)

ATTR_DOOR_STATE = 'door_state'


class OpenGarageCover(CoverDevice):
class OpenGarageCover(CoverDevice, AbstractMerossEntityWrapper):
"""Representation of a OpenGarage cover."""

def __init__(self, device: GenericGarageDoorOpener):
"""Initialize the cover."""
self._state_before_move = STATE_UNKNOWN
self._state = STATE_UNKNOWN
super().__init__(device)

# Device properties
self._device = device
self._device_id = device.uuid
self._id = device.uuid
self._device_name = self._device.name
device.register_event_callback(self.handler)
self._channel = 0

if len(self._device.get_channels())>1:
if len(self._device.get_channels()) > 1:
_LOGGER.error(f"Garage opener {self._id} has more than 1 channel. This is currently not supported.")

self._channel = 0
# Device specific state
self._state = STATE_UNKNOWN
self._state_before_move = STATE_UNKNOWN

# If the device is online, we need to update its status from STATE_UNKNOWN
if device.online and self._state == STATE_UNKNOWN:
open = device.get_status().get(self._channel)
if open:
if device.online:
self._fetch_status()

@cloud_io
def _fetch_status(self):
open = self._device.get_status().get(self._channel)
if open:
self._state = STATE_OPEN
else:
self._state = STATE_CLOSED
return self._state

def device_event_handler(self, evt):
if isinstance(evt, DeviceDoorStatusEvent) and evt.channel==self._channel:
# The underlying library only exposes "open" and "closed" statuses
if evt.door_state == 'open':
self._state = STATE_OPEN
else:
elif evt.door_state == 'closed':
self._state = STATE_CLOSED
else:
_LOGGER.error("Unknown/Invalid event door_state: %s" % evt.door_state)
else:
_LOGGER.warning("Unhandled/ignored event: %s" % str(evt))

def handler(self, evt) -> None:
if evt.event_type == MerossEventType.GARAGE_DOOR_STATUS:
if evt.channel == self._channel:
# The underlying library only exposes "open" and "closed" statuses
if evt.door_state == 'open':
self._state = STATE_OPEN
elif evt.door_state == 'closed':
self._state = STATE_CLOSED
else:
_LOGGER.error("Unknown/Invalid event door_state: %s" % evt.door_state)

# In cny case update the UI
self.async_schedule_update_ha_state(False)
# When receiving an event, let's immediately trigger the update state
self.async_schedule_update_ha_state(True)

@property
def name(self) -> str:
Expand All @@ -62,7 +68,7 @@ def name(self) -> str:
@property
def available(self) -> bool:
"""Return True if entity is available."""
return self._device.online
return self._is_online

@property
def is_closed(self):
Expand All @@ -82,36 +88,27 @@ def is_opening(self):
def is_closing(self):
return self._state == STATE_CLOSING

def _door_callback(self, error, state):
# TODO: check for errors
self.async_schedule_update_ha_state(False)

async def async_close_cover(self, **kwargs):
@cloud_io
def close_cover(self, **kwargs):
"""Close the cover."""
if self._state not in [STATE_CLOSED, STATE_CLOSING]:
self._state_before_move = self._state
self._state = STATE_CLOSING
self._device.close_door(channel=self._channel, ensure_closed=True, callback=self._door_callback)
self._device.close_door(channel=self._channel, ensure_closed=True)

async def async_open_cover(self, **kwargs):
"""Open the cover."""
if self._state not in [STATE_OPEN, STATE_OPENING]:
self._state_before_move = self._state
self._state = STATE_OPENING
self._device.open_door(channel=self._channel, ensure_opened=True, callback=self._door_callback)
# We changed the state, thus we need to notify HA about it
self.schedule_update_ha_state(True)

@cloud_io
def open_cover(self, **kwargs):
"""Open the cover."""
if self._state not in [STATE_OPEN, STATE_OPENING]:
self._state_before_move = self._state
self._state = STATE_OPENING
self._device.open_door(channel=self._channel, ensure_opened=True)

def close_cover(self, **kwargs):
"""Close the cover."""
if self._state not in [STATE_CLOSED, STATE_CLOSING]:
self._state_before_move = self._state
self._state = STATE_CLOSING
self._device.close_door(channel=self._channel, ensure_closed=True)
# We changed the state, thus we need to notify HA about it
self.schedule_update_ha_state(True)

@property
def should_poll(self) -> bool:
Expand Down
167 changes: 99 additions & 68 deletions custom_components/meross_cloud/light.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import logging

import homeassistant.util.color as color_util
from homeassistant.components.light import (Light, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP,
ATTR_HS_COLOR, ATTR_COLOR_TEMP, ATTR_BRIGHTNESS)
from meross_iot.cloud.devices.light_bulbs import GenericBulb
from meross_iot.manager import MerossManager
from meross_iot.meross_event import BulbSwitchStateChangeEvent, BulbLightStateChangeEvent

from .common import DOMAIN, MANAGER
from .common import DOMAIN, MANAGER, AbstractMerossEntityWrapper, cloud_io

_LOGGER = logging.getLogger(__name__)

Expand All @@ -25,25 +25,94 @@ def expand_status(status):
return out


class LightEntityWrapper(Light):
class LightEntityWrapper(Light, AbstractMerossEntityWrapper):
"""Wrapper class to adapt the Meross switches into the Homeassistant platform"""

def __init__(self, device: GenericBulb, channel: int):
self._device = device
super().__init__(device)

# Device state
self._state = {}
self._flags = 0

# Device info
self._channel_id = channel
self._id = self._device.uuid
self._device_name = self._device.name

self._device.register_event_callback(self.handler)

def handler(self, evt):
_LOGGER.debug("event_handler(name=%r, evt=%r)" % (self._device_name, repr(vars((evt)))))
self.async_schedule_update_ha_state(False)
# Update device state
if self._is_online:
self._state = self._fetch_state()
if self._device.supports_luminance():
self._flags |= SUPPORT_BRIGHTNESS
if self._device.is_rgb():
self._flags |= SUPPORT_COLOR
if self._device.is_light_temperature():
self._flags |= SUPPORT_COLOR_TEMP

def device_event_handler(self, evt):
if isinstance(evt, BulbSwitchStateChangeEvent):
if evt.channel == self._channel_id:
self._state['onoff'] = evt.is_on
elif isinstance(evt, BulbLightStateChangeEvent):
if evt.channel == self._channel_id:
self._state['capacity'] = evt.light_state.get('capacity')
self._state['rgb'] = evt.light_state.get('rgb')
self._state['temperature'] = evt.light_state.get('temperature')
self._state['luminance'] = evt.light_state.get('luminance')
self._state['gradual'] = evt.light_state.get('gradual')
self._state['transform'] = evt.light_state.get('transform')
else:
_LOGGER.warning("Unhandled/ignored event: %s" % str(evt))

# When receiving an event, let's immediately trigger the update state
self.async_schedule_update_ha_state(True)

@property
def available(self) -> bool:
# A device is available if it's online
return self._device.online
return self._is_online

@property
def is_on(self) -> bool:
if self._state is None:
return None

return self._state.get('onoff', None) == 1

@property
def brightness(self):
if self._state is None:
return None

# Meross bulbs support luminance between 0 and 100;
# while the HA wants values from 0 to 255. Therefore, we need to scale the values.
luminance = self._state.get('luminance', None)
if luminance is None:
return None
return float(luminance) / 100 * 255

@property
def hs_color(self):
if self._state is None:
return None

if self._state.get('capacity') == 5: # rgb mode
rgb = rgb_int_to_tuple(self._state.get('rgb'))
_LOGGER.info("rgb: %s" % str(rgb))
return color_util.color_RGB_to_hs(*rgb)
return None

@property
def color_temp(self):
if self._state is None:
return None

if self._state.get('capacity') == 6: # White light mode
value = self._state.get('temperature')
norm_value = (100 - value) / 100.0
return self.min_mireds + (norm_value * (self.max_mireds - self.min_mireds))
return None

@property
def name(self) -> str:
Expand All @@ -54,19 +123,32 @@ def unique_id(self) -> str:
return self._id

@property
def is_on(self) -> bool:
# Note that the following method is not fetching info from the device over the network.
# Instead, it is reading the device status from the state-dictionary that is handled by the library.
return self._device.get_channel_status(self._channel_id).get('onoff')
def supported_features(self):
return self._flags

@property
def device_info(self):
return {
'identifiers': {(DOMAIN, self._id)},
'name': self._device_name,
'manufacturer': 'Meross',
'model': self._device.type + " " + self._device.hwversion,
'sw_version': self._device.fwversion
}

@cloud_io
def _fetch_state(self):
self._state = self._device.get_status(self._channel_id)
return self._state

@cloud_io
def turn_off(self, **kwargs) -> None:
_LOGGER.debug('turn_off(name=%r)' % self._device_name)
self._device.turn_off(channel=self._channel_id)

@cloud_io
def turn_on(self, **kwargs) -> None:
if not self.is_on:
_LOGGER.debug('turn_on(name=%r)' % (self._device_name))
self._device.turn_on(channel=self._channel_id) # Avoid a potential response event race between setting on and below set_light_color
self._device.turn_on(channel=self._channel_id)

# Color is taken from either of these 2 values, but not both.
if ATTR_HS_COLOR in kwargs:
Expand All @@ -87,57 +169,6 @@ def turn_on(self, **kwargs) -> None:
_LOGGER.debug(" brightness change: %r" % brightness)
self._device.set_light_color(self._channel_id, luminance=brightness)

@property
def brightness(self):
# Meross bulbs support luminance between 0 and 100;
# while the HA wants values from 0 to 255. Therefore, we need to scale the values.
status = self._device.get_status(self._channel_id)
_LOGGER.debug('get_brightness(name=%r status=%r)' % (self._device_name, expand_status(status)))
return status.get('luminance') / 100 * 255

@property
def hs_color(self):
status = self._device.get_channel_status(self._channel_id)
_LOGGER.debug('get_hs_color(name=%r status=%r)' % (self._device_name, expand_status(status)))
if status.get('capacity') == 5: # rgb mode
rgb = rgb_int_to_tuple(status.get('rgb'))
return color_util.color_RGB_to_hs(*rgb)
return None

@property
def color_temp(self):
status = self._device.get_channel_status(self._channel_id)
_LOGGER.debug('get_color_temp(name=%r status=%r)' % (self._device_name, expand_status(status)))
if status.get('capacity') == 6: # White light mode
value = status.get('temperature')
norm_value = (100 - value) / 100.0
return self.min_mireds + (norm_value * (self.max_mireds - self.min_mireds))
return None

@property
def supported_features(self):
if not self.available:
return 0

flags = 0
if self._device.supports_luminance():
flags |= SUPPORT_BRIGHTNESS
if self._device.is_rgb():
flags |= SUPPORT_COLOR
if self._device.is_light_temperature():
flags |= SUPPORT_COLOR_TEMP
return flags

@property
def device_info(self):
return {
'identifiers': {(DOMAIN, self._id)},
'name': self._device_name,
'manufacturer': 'Meross',
'model': self._device.type + " " + self._device.hwversion,
'sw_version': self._device.fwversion
}


async def async_setup_entry(hass, config_entry, async_add_entities):
bulb_devices = []
Expand Down
6 changes: 4 additions & 2 deletions custom_components/meross_cloud/switch.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ def __init__(self, device: GenericPlug, channel: int):
self._entity_name = device.name

# Device specific state
self._is_on = self.get_switch_state()
self._is_on = None
if self._is_online:
self._is_on = self.fetch_switch_state()

def device_event_handler(self, evt):
if isinstance(evt, DeviceSwitchStatusEvent):
Expand Down Expand Up @@ -85,7 +87,7 @@ def turn_on(self, **kwargs) -> None:
self._device.turn_on_channel(self._channel_id)

@cloud_io
def get_switch_state(self):
def fetch_switch_state(self):
return self._device.get_channel_status(self._channel_id)


Expand Down

0 comments on commit 238dfd0

Please sign in to comment.