Xiaomi MiIO Light: Flag the device as unavailable if not reachable (#12449)

* Unavailable state introduced if the device isn't reachable.
A new configuration option "model" can be used to define the device type.

```
light:
  - platform: xiaomi_miio
    name: Xiaomi Philips Smart LED Ball
    host: 192.168.130.67
    token: da548d86f55996413d82eea94279d2ff
    # Bypass of the device model detection.
    # Optional but required to add an unavailable device
    model: philips.light.bulb
```

New attribute "scene" and "delay_off_countdown" added.
New service xiaomi_miio_set_delay_off introduced.

* Service xiaomi_miio_set_delayed_turn_off updated. The attribute "delayed_turn_off" is a timestamp now.

* None must be a valid model.

* Math.

* Microseconds removed because of the low resolution.

* Comment updated.

* Update the ATTR_DELAYED_TURN_OFF on a deviation of 4 seconds (max latency) only.

* Import of datetime fixed.

* Typo fixed.

* pylint issues fixed.

* Naming.

* Service parameter renamed.

* New ceiling lamp model (philips.light.zyceiling) added.

* Use positive timedelta instead of seconds.

* Use a unique data key per domain.
This commit is contained in:
Sebastian Muszynski 2018-02-27 21:27:52 +01:00 committed by Paulus Schoutsen
parent 4e522448b1
commit 138350fe3d
2 changed files with 134 additions and 46 deletions

View File

@ -169,3 +169,13 @@ xiaomi_miio_set_scene:
scene: scene:
description: Number of the fixed scene, between 1 and 4. description: Number of the fixed scene, between 1 and 4.
example: 1 example: 1
xiaomi_miio_set_delayed_turn_off:
description: Delayed turn off.
fields:
entity_id:
description: Name of the light entity.
example: 'light.xiaomi_miio'
time_period:
description: Time period for the delayed turn off.
example: 5, '0:05', {'minutes': 5}

View File

@ -8,6 +8,8 @@ import asyncio
from functools import partial from functools import partial
import logging import logging
from math import ceil from math import ceil
from datetime import timedelta
import datetime
import voluptuous as vol import voluptuous as vol
@ -18,16 +20,24 @@ from homeassistant.components.light import (
from homeassistant.const import (CONF_NAME, CONF_HOST, CONF_TOKEN, ) from homeassistant.const import (CONF_NAME, CONF_HOST, CONF_TOKEN, )
from homeassistant.exceptions import PlatformNotReady from homeassistant.exceptions import PlatformNotReady
from homeassistant.util import dt
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = 'Xiaomi Philips Light' DEFAULT_NAME = 'Xiaomi Philips Light'
PLATFORM = 'xiaomi_miio' DATA_KEY = 'light.xiaomi_miio'
CONF_MODEL = 'model'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string, vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_TOKEN): vol.All(cv.string, vol.Length(min=32, max=32)), vol.Required(CONF_TOKEN): vol.All(cv.string, vol.Length(min=32, max=32)),
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_MODEL): vol.In(
['philips.light.sread1',
'philips.light.ceiling',
'philips.light.zyceiling',
'philips.light.bulb']),
}) })
REQUIREMENTS = ['python-miio==0.3.7'] REQUIREMENTS = ['python-miio==0.3.7']
@ -36,25 +46,38 @@ REQUIREMENTS = ['python-miio==0.3.7']
CCT_MIN = 1 CCT_MIN = 1
CCT_MAX = 100 CCT_MAX = 100
DELAYED_TURN_OFF_MAX_DEVIATION = 4
SUCCESS = ['ok'] SUCCESS = ['ok']
ATTR_MODEL = 'model' ATTR_MODEL = 'model'
ATTR_SCENE = 'scene' ATTR_SCENE = 'scene'
ATTR_DELAYED_TURN_OFF = 'delayed_turn_off'
ATTR_TIME_PERIOD = 'time_period'
SERVICE_SET_SCENE = 'xiaomi_miio_set_scene' SERVICE_SET_SCENE = 'xiaomi_miio_set_scene'
SERVICE_SET_DELAYED_TURN_OFF = 'xiaomi_miio_set_delayed_turn_off'
XIAOMI_MIIO_SERVICE_SCHEMA = vol.Schema({ XIAOMI_MIIO_SERVICE_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
}) })
SERVICE_SCHEMA_SCENE = XIAOMI_MIIO_SERVICE_SCHEMA.extend({ SERVICE_SCHEMA_SET_SCENE = XIAOMI_MIIO_SERVICE_SCHEMA.extend({
vol.Required(ATTR_SCENE): vol.Required(ATTR_SCENE):
vol.All(vol.Coerce(int), vol.Clamp(min=1, max=4)) vol.All(vol.Coerce(int), vol.Clamp(min=1, max=4))
}) })
SERVICE_SCHEMA_SET_DELAYED_TURN_OFF = XIAOMI_MIIO_SERVICE_SCHEMA.extend({
vol.Required(ATTR_TIME_PERIOD):
vol.All(cv.time_period, cv.positive_timedelta)
})
SERVICE_TO_METHOD = { SERVICE_TO_METHOD = {
SERVICE_SET_DELAYED_TURN_OFF: {
'method': 'async_set_delayed_turn_off',
'schema': SERVICE_SCHEMA_SET_DELAYED_TURN_OFF},
SERVICE_SET_SCENE: { SERVICE_SET_SCENE: {
'method': 'async_set_scene', 'method': 'async_set_scene',
'schema': SERVICE_SCHEMA_SCENE} 'schema': SERVICE_SCHEMA_SET_SCENE},
} }
@ -63,46 +86,48 @@ SERVICE_TO_METHOD = {
def async_setup_platform(hass, config, async_add_devices, discovery_info=None): def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up the light from config.""" """Set up the light from config."""
from miio import Device, DeviceException from miio import Device, DeviceException
if PLATFORM not in hass.data: if DATA_KEY not in hass.data:
hass.data[PLATFORM] = {} hass.data[DATA_KEY] = {}
host = config.get(CONF_HOST) host = config.get(CONF_HOST)
name = config.get(CONF_NAME) name = config.get(CONF_NAME)
token = config.get(CONF_TOKEN) token = config.get(CONF_TOKEN)
model = config.get(CONF_MODEL)
_LOGGER.info("Initializing with host %s (token %s...)", host, token[:5]) _LOGGER.info("Initializing with host %s (token %s...)", host, token[:5])
try: if model is None:
light = Device(host, token) try:
device_info = light.info() miio_device = Device(host, token)
_LOGGER.info("%s %s %s initialized", device_info = miio_device.info()
device_info.model, model = device_info.model
device_info.firmware_version, _LOGGER.info("%s %s %s detected",
device_info.hardware_version) model,
device_info.firmware_version,
device_info.hardware_version)
except DeviceException:
raise PlatformNotReady
if device_info.model == 'philips.light.sread1': if model == 'philips.light.sread1':
from miio import PhilipsEyecare from miio import PhilipsEyecare
light = PhilipsEyecare(host, token) light = PhilipsEyecare(host, token)
device = XiaomiPhilipsEyecareLamp(name, light, device_info) device = XiaomiPhilipsEyecareLamp(name, light, model)
elif device_info.model == 'philips.light.ceiling': elif model in ['philips.light.ceiling', 'philips.light.zyceiling']:
from miio import Ceil from miio import Ceil
light = Ceil(host, token) light = Ceil(host, token)
device = XiaomiPhilipsCeilingLamp(name, light, device_info) device = XiaomiPhilipsCeilingLamp(name, light, model)
elif device_info.model == 'philips.light.bulb': elif model == 'philips.light.bulb':
from miio import PhilipsBulb from miio import PhilipsBulb
light = PhilipsBulb(host, token) light = PhilipsBulb(host, token)
device = XiaomiPhilipsLightBall(name, light, device_info) device = XiaomiPhilipsLightBall(name, light, model)
else: else:
_LOGGER.error( _LOGGER.error(
'Unsupported device found! Please create an issue at ' 'Unsupported device found! Please create an issue at '
'https://github.com/rytilahti/python-miio/issues ' 'https://github.com/rytilahti/python-miio/issues '
'and provide the following data: %s', device_info.model) 'and provide the following data: %s', model)
return False return False
except DeviceException: hass.data[DATA_KEY][host] = device
raise PlatformNotReady
hass.data[PLATFORM][host] = device
async_add_devices([device], update_before_add=True) async_add_devices([device], update_before_add=True)
@asyncio.coroutine @asyncio.coroutine
@ -113,10 +138,10 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
if key != ATTR_ENTITY_ID} if key != ATTR_ENTITY_ID}
entity_ids = service.data.get(ATTR_ENTITY_ID) entity_ids = service.data.get(ATTR_ENTITY_ID)
if entity_ids: if entity_ids:
target_devices = [dev for dev in hass.data[PLATFORM].values() target_devices = [dev for dev in hass.data[DATA_KEY].values()
if dev.entity_id in entity_ids] if dev.entity_id in entity_ids]
else: else:
target_devices = hass.data[PLATFORM].values() target_devices = hass.data[DATA_KEY].values()
update_tasks = [] update_tasks = []
for target_device in target_devices: for target_device in target_devices:
@ -136,10 +161,10 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
class XiaomiPhilipsGenericLight(Light): class XiaomiPhilipsGenericLight(Light):
"""Representation of a Xiaomi Philips Light.""" """Representation of a Xiaomi Philips Light."""
def __init__(self, name, light, device_info): def __init__(self, name, light, model):
"""Initialize the light device.""" """Initialize the light device."""
self._name = name self._name = name
self._device_info = device_info self._model = model
self._brightness = None self._brightness = None
self._color_temp = None self._color_temp = None
@ -147,7 +172,9 @@ class XiaomiPhilipsGenericLight(Light):
self._light = light self._light = light
self._state = None self._state = None
self._state_attrs = { self._state_attrs = {
ATTR_MODEL: self._device_info.model, ATTR_MODEL: self._model,
ATTR_SCENE: None,
ATTR_DELAYED_TURN_OFF: None,
} }
@property @property
@ -217,14 +244,14 @@ class XiaomiPhilipsGenericLight(Light):
if result: if result:
self._brightness = brightness self._brightness = brightness
else:
self._state = yield from self._try_command( yield from self._try_command(
"Turning the light on failed.", self._light.on) "Turning the light on failed.", self._light.on)
@asyncio.coroutine @asyncio.coroutine
def async_turn_off(self, **kwargs): def async_turn_off(self, **kwargs):
"""Turn the light off.""" """Turn the light off."""
self._state = yield from self._try_command( yield from self._try_command(
"Turning the light off failed.", self._light.off) "Turning the light off failed.", self._light.off)
@asyncio.coroutine @asyncio.coroutine
@ -236,9 +263,20 @@ class XiaomiPhilipsGenericLight(Light):
_LOGGER.debug("Got new state: %s", state) _LOGGER.debug("Got new state: %s", state)
self._state = state.is_on self._state = state.is_on
self._brightness = ceil((255/100.0) * state.brightness) self._brightness = ceil((255 / 100.0) * state.brightness)
delayed_turn_off = self.delayed_turn_off_timestamp(
state.delay_off_countdown,
dt.utcnow(),
self._state_attrs[ATTR_DELAYED_TURN_OFF])
self._state_attrs.update({
ATTR_SCENE: state.scene,
ATTR_DELAYED_TURN_OFF: delayed_turn_off,
})
except DeviceException as ex: except DeviceException as ex:
self._state = None
_LOGGER.error("Got exception while fetching the state: %s", ex) _LOGGER.error("Got exception while fetching the state: %s", ex)
@asyncio.coroutine @asyncio.coroutine
@ -248,6 +286,13 @@ class XiaomiPhilipsGenericLight(Light):
"Setting a fixed scene failed.", "Setting a fixed scene failed.",
self._light.set_scene, scene) self._light.set_scene, scene)
@asyncio.coroutine
def async_set_delayed_turn_off(self, time_period: timedelta):
"""Set delay off. The unit is different per device."""
yield from self._try_command(
"Setting the delay off failed.",
self._light.delay_off, time_period.total_seconds())
@staticmethod @staticmethod
def translate(value, left_min, left_max, right_min, right_max): def translate(value, left_min, left_max, right_min, right_max):
"""Map a value from left span to right span.""" """Map a value from left span to right span."""
@ -256,6 +301,28 @@ class XiaomiPhilipsGenericLight(Light):
value_scaled = float(value - left_min) / float(left_span) value_scaled = float(value - left_min) / float(left_span)
return int(right_min + (value_scaled * right_span)) return int(right_min + (value_scaled * right_span))
@staticmethod
def delayed_turn_off_timestamp(countdown: int,
current: datetime,
previous: datetime):
"""Update the turn off timestamp only if necessary."""
if countdown > 0:
new = current.replace(microsecond=0) + \
timedelta(seconds=countdown)
if previous is None:
return new
lower = timedelta(seconds=-DELAYED_TURN_OFF_MAX_DEVIATION)
upper = timedelta(seconds=DELAYED_TURN_OFF_MAX_DEVIATION)
diff = previous - new
if lower < diff < upper:
return previous
return new
return None
class XiaomiPhilipsLightBall(XiaomiPhilipsGenericLight, Light): class XiaomiPhilipsLightBall(XiaomiPhilipsGenericLight, Light):
"""Representation of a Xiaomi Philips Light Ball.""" """Representation of a Xiaomi Philips Light Ball."""
@ -339,7 +406,7 @@ class XiaomiPhilipsLightBall(XiaomiPhilipsGenericLight, Light):
self._brightness = brightness self._brightness = brightness
else: else:
self._state = yield from self._try_command( yield from self._try_command(
"Turning the light on failed.", self._light.on) "Turning the light on failed.", self._light.on)
@asyncio.coroutine @asyncio.coroutine
@ -351,13 +418,24 @@ class XiaomiPhilipsLightBall(XiaomiPhilipsGenericLight, Light):
_LOGGER.debug("Got new state: %s", state) _LOGGER.debug("Got new state: %s", state)
self._state = state.is_on self._state = state.is_on
self._brightness = ceil((255/100.0) * state.brightness) self._brightness = ceil((255 / 100.0) * state.brightness)
self._color_temp = self.translate( self._color_temp = self.translate(
state.color_temperature, state.color_temperature,
CCT_MIN, CCT_MAX, CCT_MIN, CCT_MAX,
self.max_mireds, self.min_mireds) self.max_mireds, self.min_mireds)
delayed_turn_off = self.delayed_turn_off_timestamp(
state.delay_off_countdown,
dt.utcnow(),
self._state_attrs[ATTR_DELAYED_TURN_OFF])
self._state_attrs.update({
ATTR_SCENE: state.scene,
ATTR_DELAYED_TURN_OFF: delayed_turn_off,
})
except DeviceException as ex: except DeviceException as ex:
self._state = None
_LOGGER.error("Got exception while fetching the state: %s", ex) _LOGGER.error("Got exception while fetching the state: %s", ex)