Merge pull request #8270 from home-assistant/release-0-48

0.48
This commit is contained in:
Paulus Schoutsen 2017-07-01 16:58:10 -07:00 committed by GitHub
commit 7461c57542
173 changed files with 6458 additions and 3464 deletions

View File

@ -35,23 +35,35 @@ omit =
homeassistant/components/bloomsky.py homeassistant/components/bloomsky.py
homeassistant/components/*/bloomsky.py homeassistant/components/*/bloomsky.py
homeassistant/components/comfoconnect.py
homeassistant/components/*/comfoconnect.py
homeassistant/components/digital_ocean.py homeassistant/components/digital_ocean.py
homeassistant/components/*/digital_ocean.py homeassistant/components/*/digital_ocean.py
homeassistant/components/dweet.py homeassistant/components/dweet.py
homeassistant/components/*/dweet.py homeassistant/components/*/dweet.py
homeassistant/components/eight_sleep.py
homeassistant/components/*/eight_sleep.py
homeassistant/components/ecobee.py homeassistant/components/ecobee.py
homeassistant/components/*/ecobee.py homeassistant/components/*/ecobee.py
homeassistant/components/enocean.py
homeassistant/components/*/enocean.py
homeassistant/components/envisalink.py homeassistant/components/envisalink.py
homeassistant/components/*/envisalink.py homeassistant/components/*/envisalink.py
homeassistant/components/google.py homeassistant/components/google.py
homeassistant/components/*/google.py homeassistant/components/*/google.py
homeassistant/components/insteon_hub.py homeassistant/components/hdmi_cec.py
homeassistant/components/*/insteon_hub.py homeassistant/components/*/hdmi_cec.py
homeassistant/components/homematic.py
homeassistant/components/*/homematic.py
homeassistant/components/insteon_local.py homeassistant/components/insteon_local.py
homeassistant/components/*/insteon_local.py homeassistant/components/*/insteon_local.py
@ -65,12 +77,18 @@ omit =
homeassistant/components/isy994.py homeassistant/components/isy994.py
homeassistant/components/*/isy994.py homeassistant/components/*/isy994.py
homeassistant/components/joaoapps_join.py
homeassistant/components/*/joaoapps_join.py
homeassistant/components/juicenet.py homeassistant/components/juicenet.py
homeassistant/components/*/juicenet.py homeassistant/components/*/juicenet.py
homeassistant/components/kira.py homeassistant/components/kira.py
homeassistant/components/*/kira.py homeassistant/components/*/kira.py
homeassistant/components/knx.py
homeassistant/components/*/knx.py
homeassistant/components/lutron.py homeassistant/components/lutron.py
homeassistant/components/*/lutron.py homeassistant/components/*/lutron.py
@ -80,15 +98,27 @@ omit =
homeassistant/components/mailgun.py homeassistant/components/mailgun.py
homeassistant/components/*/mailgun.py homeassistant/components/*/mailgun.py
homeassistant/components/maxcube.py
homeassistant/components/*/maxcube.py
homeassistant/components/mochad.py
homeassistant/components/*/mochad.py
homeassistant/components/modbus.py homeassistant/components/modbus.py
homeassistant/components/*/modbus.py homeassistant/components/*/modbus.py
homeassistant/components/mysensors.py homeassistant/components/mysensors.py
homeassistant/components/*/mysensors.py homeassistant/components/*/mysensors.py
homeassistant/components/neato.py
homeassistant/components/*/neato.py
homeassistant/components/nest.py homeassistant/components/nest.py
homeassistant/components/*/nest.py homeassistant/components/*/nest.py
homeassistant/components/netatmo.py
homeassistant/components/*/netatmo.py
homeassistant/components/octoprint.py homeassistant/components/octoprint.py
homeassistant/components/*/octoprint.py homeassistant/components/*/octoprint.py
@ -116,6 +146,9 @@ omit =
homeassistant/components/scsgate.py homeassistant/components/scsgate.py
homeassistant/components/*/scsgate.py homeassistant/components/*/scsgate.py
homeassistant/components/tado.py
homeassistant/components/*/tado.py
homeassistant/components/tellduslive.py homeassistant/components/tellduslive.py
homeassistant/components/*/tellduslive.py homeassistant/components/*/tellduslive.py
@ -148,45 +181,18 @@ omit =
homeassistant/components/wink.py homeassistant/components/wink.py
homeassistant/components/*/wink.py homeassistant/components/*/wink.py
homeassistant/components/zigbee.py
homeassistant/components/*/zigbee.py
homeassistant/components/enocean.py
homeassistant/components/*/enocean.py
homeassistant/components/netatmo.py
homeassistant/components/*/netatmo.py
homeassistant/components/neato.py
homeassistant/components/*/neato.py
homeassistant/components/homematic.py
homeassistant/components/*/homematic.py
homeassistant/components/knx.py
homeassistant/components/*/knx.py
homeassistant/components/zoneminder.py
homeassistant/components/*/zoneminder.py
homeassistant/components/mochad.py
homeassistant/components/*/mochad.py
homeassistant/components/zabbix.py homeassistant/components/zabbix.py
homeassistant/components/*/zabbix.py homeassistant/components/*/zabbix.py
homeassistant/components/maxcube.py
homeassistant/components/*/maxcube.py
homeassistant/components/tado.py
homeassistant/components/*/tado.py
homeassistant/components/zha/__init__.py homeassistant/components/zha/__init__.py
homeassistant/components/zha/const.py homeassistant/components/zha/const.py
homeassistant/components/*/zha.py homeassistant/components/*/zha.py
homeassistant/components/eight_sleep.py homeassistant/components/zigbee.py
homeassistant/components/*/eight_sleep.py homeassistant/components/*/zigbee.py
homeassistant/components/zoneminder.py
homeassistant/components/*/zoneminder.py
homeassistant/components/alarm_control_panel/alarmdotcom.py homeassistant/components/alarm_control_panel/alarmdotcom.py
homeassistant/components/alarm_control_panel/concord232.py homeassistant/components/alarm_control_panel/concord232.py
@ -224,11 +230,11 @@ omit =
homeassistant/components/climate/sensibo.py homeassistant/components/climate/sensibo.py
homeassistant/components/cover/garadget.py homeassistant/components/cover/garadget.py
homeassistant/components/cover/homematic.py homeassistant/components/cover/homematic.py
homeassistant/components/cover/knx.py
homeassistant/components/cover/myq.py homeassistant/components/cover/myq.py
homeassistant/components/cover/opengarage.py homeassistant/components/cover/opengarage.py
homeassistant/components/cover/rpi_gpio.py homeassistant/components/cover/rpi_gpio.py
homeassistant/components/cover/scsgate.py homeassistant/components/cover/scsgate.py
homeassistant/components/cover/wink.py
homeassistant/components/device_tracker/actiontec.py homeassistant/components/device_tracker/actiontec.py
homeassistant/components/device_tracker/aruba.py homeassistant/components/device_tracker/aruba.py
homeassistant/components/device_tracker/asuswrt.py homeassistant/components/device_tracker/asuswrt.py
@ -242,6 +248,7 @@ omit =
homeassistant/components/device_tracker/gpslogger.py homeassistant/components/device_tracker/gpslogger.py
homeassistant/components/device_tracker/icloud.py homeassistant/components/device_tracker/icloud.py
homeassistant/components/device_tracker/linksys_ap.py homeassistant/components/device_tracker/linksys_ap.py
homeassistant/components/device_tracker/linksys_smart.py
homeassistant/components/device_tracker/luci.py homeassistant/components/device_tracker/luci.py
homeassistant/components/device_tracker/mikrotik.py homeassistant/components/device_tracker/mikrotik.py
homeassistant/components/device_tracker/netgear.py homeassistant/components/device_tracker/netgear.py
@ -263,12 +270,10 @@ omit =
homeassistant/components/fan/mqtt.py homeassistant/components/fan/mqtt.py
homeassistant/components/feedreader.py homeassistant/components/feedreader.py
homeassistant/components/foursquare.py homeassistant/components/foursquare.py
homeassistant/components/hdmi_cec.py
homeassistant/components/ifttt.py homeassistant/components/ifttt.py
homeassistant/components/image_processing/dlib_face_detect.py homeassistant/components/image_processing/dlib_face_detect.py
homeassistant/components/image_processing/dlib_face_identify.py homeassistant/components/image_processing/dlib_face_identify.py
homeassistant/components/image_processing/seven_segments.py homeassistant/components/image_processing/seven_segments.py
homeassistant/components/joaoapps_join.py
homeassistant/components/keyboard.py homeassistant/components/keyboard.py
homeassistant/components/keyboard_remote.py homeassistant/components/keyboard_remote.py
homeassistant/components/light/avion.py homeassistant/components/light/avion.py
@ -278,7 +283,7 @@ omit =
homeassistant/components/light/flux_led.py homeassistant/components/light/flux_led.py
homeassistant/components/light/hue.py homeassistant/components/light/hue.py
homeassistant/components/light/hyperion.py homeassistant/components/light/hyperion.py
homeassistant/components/light/lifx/*.py homeassistant/components/light/lifx.py
homeassistant/components/light/lifx_legacy.py homeassistant/components/light/lifx_legacy.py
homeassistant/components/light/limitlessled.py homeassistant/components/light/limitlessled.py
homeassistant/components/light/mystrom.py homeassistant/components/light/mystrom.py
@ -312,7 +317,6 @@ omit =
homeassistant/components/media_player/frontier_silicon.py homeassistant/components/media_player/frontier_silicon.py
homeassistant/components/media_player/gpmdp.py homeassistant/components/media_player/gpmdp.py
homeassistant/components/media_player/gstreamer.py homeassistant/components/media_player/gstreamer.py
homeassistant/components/media_player/hdmi_cec.py
homeassistant/components/media_player/itunes.py homeassistant/components/media_player/itunes.py
homeassistant/components/media_player/kodi.py homeassistant/components/media_player/kodi.py
homeassistant/components/media_player/lg_netcast.py homeassistant/components/media_player/lg_netcast.py
@ -342,13 +346,13 @@ omit =
homeassistant/components/notify/aws_sns.py homeassistant/components/notify/aws_sns.py
homeassistant/components/notify/aws_sqs.py homeassistant/components/notify/aws_sqs.py
homeassistant/components/notify/ciscospark.py homeassistant/components/notify/ciscospark.py
homeassistant/components/notify/clicksend.py
homeassistant/components/notify/discord.py homeassistant/components/notify/discord.py
homeassistant/components/notify/facebook.py homeassistant/components/notify/facebook.py
homeassistant/components/notify/free_mobile.py homeassistant/components/notify/free_mobile.py
homeassistant/components/notify/gntp.py homeassistant/components/notify/gntp.py
homeassistant/components/notify/group.py homeassistant/components/notify/group.py
homeassistant/components/notify/instapush.py homeassistant/components/notify/instapush.py
homeassistant/components/notify/joaoapps_join.py
homeassistant/components/notify/kodi.py homeassistant/components/notify/kodi.py
homeassistant/components/notify/lannouncer.py homeassistant/components/notify/lannouncer.py
homeassistant/components/notify/llamalab_automate.py homeassistant/components/notify/llamalab_automate.py
@ -379,8 +383,10 @@ omit =
homeassistant/components/sensor/arest.py homeassistant/components/sensor/arest.py
homeassistant/components/sensor/arwn.py homeassistant/components/sensor/arwn.py
homeassistant/components/sensor/bbox.py homeassistant/components/sensor/bbox.py
homeassistant/components/sensor/bh1750.py
homeassistant/components/sensor/bitcoin.py homeassistant/components/sensor/bitcoin.py
homeassistant/components/sensor/blockchain.py homeassistant/components/sensor/blockchain.py
homeassistant/components/sensor/bme280.py
homeassistant/components/sensor/bom.py homeassistant/components/sensor/bom.py
homeassistant/components/sensor/broadlink.py homeassistant/components/sensor/broadlink.py
homeassistant/components/sensor/buienradar.py homeassistant/components/sensor/buienradar.py
@ -419,6 +425,7 @@ omit =
homeassistant/components/sensor/haveibeenpwned.py homeassistant/components/sensor/haveibeenpwned.py
homeassistant/components/sensor/hddtemp.py homeassistant/components/sensor/hddtemp.py
homeassistant/components/sensor/hp_ilo.py homeassistant/components/sensor/hp_ilo.py
homeassistant/components/sensor/htu21d.py
homeassistant/components/sensor/hydroquebec.py homeassistant/components/sensor/hydroquebec.py
homeassistant/components/sensor/imap.py homeassistant/components/sensor/imap.py
homeassistant/components/sensor/imap_email_content.py homeassistant/components/sensor/imap_email_content.py
@ -473,6 +480,7 @@ omit =
homeassistant/components/sensor/transmission.py homeassistant/components/sensor/transmission.py
homeassistant/components/sensor/twitch.py homeassistant/components/sensor/twitch.py
homeassistant/components/sensor/uber.py homeassistant/components/sensor/uber.py
homeassistant/components/sensor/upnp.py
homeassistant/components/sensor/ups.py homeassistant/components/sensor/ups.py
homeassistant/components/sensor/usps.py homeassistant/components/sensor/usps.py
homeassistant/components/sensor/vasttrafik.py homeassistant/components/sensor/vasttrafik.py
@ -480,6 +488,7 @@ omit =
homeassistant/components/sensor/xbox_live.py homeassistant/components/sensor/xbox_live.py
homeassistant/components/sensor/yweather.py homeassistant/components/sensor/yweather.py
homeassistant/components/sensor/zamg.py homeassistant/components/sensor/zamg.py
homeassistant/components/shiftr.py
homeassistant/components/spc.py homeassistant/components/spc.py
homeassistant/components/switch/acer_projector.py homeassistant/components/switch/acer_projector.py
homeassistant/components/switch/anel_pwrctrl.py homeassistant/components/switch/anel_pwrctrl.py
@ -489,7 +498,6 @@ omit =
homeassistant/components/switch/dlink.py homeassistant/components/switch/dlink.py
homeassistant/components/switch/edimax.py homeassistant/components/switch/edimax.py
homeassistant/components/switch/fritzdect.py homeassistant/components/switch/fritzdect.py
homeassistant/components/switch/hdmi_cec.py
homeassistant/components/switch/hikvisioncam.py homeassistant/components/switch/hikvisioncam.py
homeassistant/components/switch/hook.py homeassistant/components/switch/hook.py
homeassistant/components/switch/kankun.py homeassistant/components/switch/kankun.py

View File

@ -1,2 +1,14 @@
.tox # General files
.git .git
.github
config
# Test related files
.tox
# Other virtualization methods
venv
.vagrant
# Temporary files
**/__pycache__

View File

@ -1,3 +1,7 @@
# Notice:
# When updating this file, please also update virtualization/Docker/Dockerfile.dev
# This way, the development image and the production image are kept in sync.
FROM python:3.6 FROM python:3.6
MAINTAINER Paulus Schoutsen <Paulus@PaulusSchoutsen.nl> MAINTAINER Paulus Schoutsen <Paulus@PaulusSchoutsen.nl>
@ -21,8 +25,12 @@ RUN virtualization/Docker/setup_docker_prereqs
# Install hass component dependencies # Install hass component dependencies
COPY requirements_all.txt requirements_all.txt COPY requirements_all.txt requirements_all.txt
# Uninstall enum34 because some depenndecies install it but breaks Python 3.4+.
# See PR #8103 for more info.
RUN pip3 install --no-cache-dir -r requirements_all.txt && \ RUN pip3 install --no-cache-dir -r requirements_all.txt && \
pip3 install --no-cache-dir mysqlclient psycopg2 uvloop cchardet pip3 install --no-cache-dir mysqlclient psycopg2 uvloop cchardet && \
pip3 uninstall -y enum34
# Copy source # Copy source
COPY . . COPY . .

View File

@ -5,6 +5,7 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/alarm_control_panel.verisure/ https://home-assistant.io/components/alarm_control_panel.verisure/
""" """
import logging import logging
from time import sleep
import homeassistant.components.alarm_control_panel as alarm import homeassistant.components.alarm_control_panel as alarm
from homeassistant.components.verisure import HUB as hub from homeassistant.components.verisure import HUB as hub
@ -20,20 +21,29 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Verisure platform.""" """Set up the Verisure platform."""
alarms = [] alarms = []
if int(hub.config.get(CONF_ALARM, 1)): if int(hub.config.get(CONF_ALARM, 1)):
hub.update_alarms() hub.update_overview()
alarms.extend([ alarms.append(VerisureAlarm())
VerisureAlarm(value.id)
for value in hub.alarm_status.values()
])
add_devices(alarms) add_devices(alarms)
def set_arm_state(state, code=None):
"""Send set arm state command."""
transaction_id = hub.session.set_arm_state(code, state)[
'armStateChangeTransactionId']
_LOGGER.info('verisure set arm state %s', state)
transaction = {}
while 'result' not in transaction:
sleep(0.5)
transaction = hub.session.get_arm_state_transaction(transaction_id)
# pylint: disable=unexpected-keyword-arg
hub.update_overview(no_throttle=True)
class VerisureAlarm(alarm.AlarmControlPanel): class VerisureAlarm(alarm.AlarmControlPanel):
"""Representation of a Verisure alarm status.""" """Representation of a Verisure alarm status."""
def __init__(self, device_id): def __init__(self):
"""Initialize the Verisure alarm panel.""" """Initalize the Verisure alarm panel."""
self._id = device_id
self._state = STATE_UNKNOWN self._state = STATE_UNKNOWN
self._digits = hub.config.get(CONF_CODE_DIGITS) self._digits = hub.config.get(CONF_CODE_DIGITS)
self._changed_by = None self._changed_by = None
@ -41,18 +51,13 @@ class VerisureAlarm(alarm.AlarmControlPanel):
@property @property
def name(self): def name(self):
"""Return the name of the device.""" """Return the name of the device."""
return 'Alarm {}'.format(self._id) return '{} alarm'.format(hub.session.installations[0]['alias'])
@property @property
def state(self): def state(self):
"""Return the state of the device.""" """Return the state of the device."""
return self._state return self._state
@property
def available(self):
"""Return True if entity is available."""
return hub.available
@property @property
def code_format(self): def code_format(self):
"""Return the code format as regex.""" """Return the code format as regex."""
@ -65,33 +70,26 @@ class VerisureAlarm(alarm.AlarmControlPanel):
def update(self): def update(self):
"""Update alarm status.""" """Update alarm status."""
hub.update_alarms() hub.update_overview()
status = hub.get_first("$.armState.statusType")
if hub.alarm_status[self._id].status == 'unarmed': if status == 'DISARMED':
self._state = STATE_ALARM_DISARMED self._state = STATE_ALARM_DISARMED
elif hub.alarm_status[self._id].status == 'armedhome': elif status == 'ARMED_HOME':
self._state = STATE_ALARM_ARMED_HOME self._state = STATE_ALARM_ARMED_HOME
elif hub.alarm_status[self._id].status == 'armed': elif status == 'ARMED_AWAY':
self._state = STATE_ALARM_ARMED_AWAY self._state = STATE_ALARM_ARMED_AWAY
elif hub.alarm_status[self._id].status != 'pending': elif status != 'PENDING':
_LOGGER.error( _LOGGER.error('Unknown alarm state %s', status)
"Unknown alarm state %s", hub.alarm_status[self._id].status) self._changed_by = hub.get_first("$.armState.name")
self._changed_by = hub.alarm_status[self._id].name
def alarm_disarm(self, code=None): def alarm_disarm(self, code=None):
"""Send disarm command.""" """Send disarm command."""
hub.my_pages.alarm.set(code, 'DISARMED') set_arm_state('DISARMED', code)
_LOGGER.info("Verisure alarm disarming")
hub.my_pages.alarm.wait_while_pending()
def alarm_arm_home(self, code=None): def alarm_arm_home(self, code=None):
"""Send arm home command.""" """Send arm home command."""
hub.my_pages.alarm.set(code, 'ARMED_HOME') set_arm_state('ARMED_HOME', code)
_LOGGER.info("Verisure alarm arming home")
hub.my_pages.alarm.wait_while_pending()
def alarm_arm_away(self, code=None): def alarm_arm_away(self, code=None):
"""Send arm away command.""" """Send arm away command."""
hub.my_pages.alarm.set(code, 'ARMED_AWAY') set_arm_state('ARMED_AWAY', code)
_LOGGER.info("Verisure alarm arming away")
hub.my_pages.alarm.wait_while_pending()

View File

@ -25,6 +25,7 @@ _LOGGER = logging.getLogger(__name__)
DOMAIN = 'alert' DOMAIN = 'alert'
ENTITY_ID_FORMAT = DOMAIN + '.{}' ENTITY_ID_FORMAT = DOMAIN + '.{}'
CONF_DONE_MESSAGE = 'done_message'
CONF_CAN_ACK = 'can_acknowledge' CONF_CAN_ACK = 'can_acknowledge'
CONF_NOTIFIERS = 'notifiers' CONF_NOTIFIERS = 'notifiers'
CONF_REPEAT = 'repeat' CONF_REPEAT = 'repeat'
@ -35,6 +36,7 @@ DEFAULT_SKIP_FIRST = False
ALERT_SCHEMA = vol.Schema({ ALERT_SCHEMA = vol.Schema({
vol.Required(CONF_NAME): cv.string, vol.Required(CONF_NAME): cv.string,
vol.Optional(CONF_DONE_MESSAGE, default=None): cv.string,
vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Required(CONF_ENTITY_ID): cv.entity_id,
vol.Required(CONF_STATE, default=STATE_ON): cv.string, vol.Required(CONF_STATE, default=STATE_ON): cv.string,
vol.Required(CONF_REPEAT): vol.All(cv.ensure_list, [vol.Coerce(float)]), vol.Required(CONF_REPEAT): vol.All(cv.ensure_list, [vol.Coerce(float)]),
@ -121,10 +123,10 @@ def async_setup(hass, config):
# Setup alerts # Setup alerts
for entity_id, alert in alerts.items(): for entity_id, alert in alerts.items():
entity = Alert(hass, entity_id, entity = Alert(hass, entity_id,
alert[CONF_NAME], alert[CONF_ENTITY_ID], alert[CONF_NAME], alert[CONF_DONE_MESSAGE],
alert[CONF_STATE], alert[CONF_REPEAT], alert[CONF_ENTITY_ID], alert[CONF_STATE],
alert[CONF_SKIP_FIRST], alert[CONF_NOTIFIERS], alert[CONF_REPEAT], alert[CONF_SKIP_FIRST],
alert[CONF_CAN_ACK]) alert[CONF_NOTIFIERS], alert[CONF_CAN_ACK])
all_alerts[entity.entity_id] = entity all_alerts[entity.entity_id] = entity
# Read descriptions # Read descriptions
@ -154,8 +156,8 @@ def async_setup(hass, config):
class Alert(ToggleEntity): class Alert(ToggleEntity):
"""Representation of an alert.""" """Representation of an alert."""
def __init__(self, hass, entity_id, name, watched_entity_id, state, def __init__(self, hass, entity_id, name, done_message, watched_entity_id,
repeat, skip_first, notifiers, can_ack): state, repeat, skip_first, notifiers, can_ack):
"""Initialize the alert.""" """Initialize the alert."""
self.hass = hass self.hass = hass
self._name = name self._name = name
@ -163,6 +165,7 @@ class Alert(ToggleEntity):
self._skip_first = skip_first self._skip_first = skip_first
self._notifiers = notifiers self._notifiers = notifiers
self._can_ack = can_ack self._can_ack = can_ack
self._done_message = done_message
self._delay = [timedelta(minutes=val) for val in repeat] self._delay = [timedelta(minutes=val) for val in repeat]
self._next_delay = 0 self._next_delay = 0
@ -170,6 +173,7 @@ class Alert(ToggleEntity):
self._firing = False self._firing = False
self._ack = False self._ack = False
self._cancel = None self._cancel = None
self._send_done_message = False
self.entity_id = ENTITY_ID_FORMAT.format(entity_id) self.entity_id = ENTITY_ID_FORMAT.format(entity_id)
event.async_track_state_change( event.async_track_state_change(
@ -230,6 +234,8 @@ class Alert(ToggleEntity):
self._cancel() self._cancel()
self._ack = False self._ack = False
self._firing = False self._firing = False
if self._done_message and self._send_done_message:
yield from self._notify_done_message()
self.hass.async_add_job(self.async_update_ha_state) self.hass.async_add_job(self.async_update_ha_state)
@asyncio.coroutine @asyncio.coroutine
@ -249,11 +255,21 @@ class Alert(ToggleEntity):
if not self._ack: if not self._ack:
_LOGGER.info("Alerting: %s", self._name) _LOGGER.info("Alerting: %s", self._name)
self._send_done_message = True
for target in self._notifiers: for target in self._notifiers:
yield from self.hass.services.async_call( yield from self.hass.services.async_call(
'notify', target, {'message': self._name}) 'notify', target, {'message': self._name})
yield from self._schedule_notify() yield from self._schedule_notify()
@asyncio.coroutine
def _notify_done_message(self, *args):
"""Send notification of complete alert."""
_LOGGER.info("Alerting: %s", self._done_message)
self._send_done_message = False
for target in self._notifiers:
yield from self.hass.services.async_call(
'notify', target, {'message': self._done_message})
@asyncio.coroutine @asyncio.coroutine
def async_turn_on(self): def async_turn_on(self):
"""Async Unacknowledge alert.""" """Async Unacknowledge alert."""

View File

@ -1,27 +1,27 @@
""" """
This component provides basic support for Netgear Arlo IP cameras. This component provides basic support for Netgear Arlo IP cameras.
For more details about this platform, please refer to the documentation at For more details about this component, please refer to the documentation at
https://home-assistant.io/components/arlo/ https://home-assistant.io/components/arlo/
""" """
import logging import logging
import voluptuous as vol import voluptuous as vol
from homeassistant.helpers import config_validation as cv
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
import homeassistant.loader as loader
from requests.exceptions import HTTPError, ConnectTimeout from requests.exceptions import HTTPError, ConnectTimeout
import homeassistant.loader as loader
from homeassistant.helpers import config_validation as cv
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
REQUIREMENTS = ['pyarlo==0.0.4'] REQUIREMENTS = ['pyarlo==0.0.4']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONF_ATTRIBUTION = 'Data provided by arlo.netgear.com' CONF_ATTRIBUTION = "Data provided by arlo.netgear.com"
DOMAIN = 'arlo'
DATA_ARLO = 'data_arlo'
DEFAULT_BRAND = 'Netgear Arlo' DEFAULT_BRAND = 'Netgear Arlo'
DOMAIN = 'arlo'
NOTIFICATION_ID = 'arlo_notification' NOTIFICATION_ID = 'arlo_notification'
NOTIFICATION_TITLE = 'Arlo Camera Setup' NOTIFICATION_TITLE = 'Arlo Camera Setup'
@ -47,7 +47,7 @@ def setup(hass, config):
arlo = PyArlo(username, password, preload=False) arlo = PyArlo(username, password, preload=False)
if not arlo.is_connected: if not arlo.is_connected:
return False return False
hass.data['arlo'] = arlo hass.data[DATA_ARLO] = arlo
except (ConnectTimeout, HTTPError) as ex: except (ConnectTimeout, HTTPError) as ex:
_LOGGER.error("Unable to connect to Netgar Arlo: %s", str(ex)) _LOGGER.error("Unable to connect to Netgar Arlo: %s", str(ex))
persistent_notification.create( persistent_notification.create(

View File

@ -11,6 +11,7 @@ import os
import voluptuous as vol import voluptuous as vol
from homeassistant.config import load_yaml_config_file
from homeassistant.const import (ATTR_LOCATION, ATTR_TRIPPED, from homeassistant.const import (ATTR_LOCATION, ATTR_TRIPPED,
CONF_HOST, CONF_INCLUDE, CONF_NAME, CONF_HOST, CONF_INCLUDE, CONF_NAME,
CONF_PASSWORD, CONF_TRIGGER_TIME, CONF_PASSWORD, CONF_TRIGGER_TIME,
@ -18,11 +19,12 @@ from homeassistant.const import (ATTR_LOCATION, ATTR_TRIPPED,
from homeassistant.components.discovery import SERVICE_AXIS from homeassistant.components.discovery import SERVICE_AXIS
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.helpers import discovery from homeassistant.helpers import discovery
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.loader import get_component from homeassistant.loader import get_component
REQUIREMENTS = ['axis==7'] REQUIREMENTS = ['axis==8']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -59,6 +61,21 @@ CONFIG_SCHEMA = vol.Schema({
}), }),
}, extra=vol.ALLOW_EXTRA) }, extra=vol.ALLOW_EXTRA)
SERVICE_VAPIX_CALL = 'vapix_call'
SERVICE_VAPIX_CALL_RESPONSE = 'vapix_call_response'
SERVICE_CGI = 'cgi'
SERVICE_ACTION = 'action'
SERVICE_PARAM = 'param'
SERVICE_DEFAULT_CGI = 'param.cgi'
SERVICE_DEFAULT_ACTION = 'update'
SERVICE_SCHEMA = vol.Schema({
vol.Required(CONF_NAME): cv.string,
vol.Required(SERVICE_PARAM): cv.string,
vol.Optional(SERVICE_CGI, default=SERVICE_DEFAULT_CGI): cv.string,
vol.Optional(SERVICE_ACTION, default=SERVICE_DEFAULT_ACTION): cv.string,
})
def request_configuration(hass, name, host, serialnumber): def request_configuration(hass, name, host, serialnumber):
"""Request configuration steps from the user.""" """Request configuration steps from the user."""
@ -135,23 +152,34 @@ def setup(hass, base_config):
def axis_device_discovered(service, discovery_info): def axis_device_discovered(service, discovery_info):
"""Called when axis devices has been found.""" """Called when axis devices has been found."""
host = discovery_info['host'] host = discovery_info[CONF_HOST]
name = discovery_info['hostname'] name = discovery_info['hostname']
serialnumber = discovery_info['properties']['macaddress'] serialnumber = discovery_info['properties']['macaddress']
if serialnumber not in AXIS_DEVICES: if serialnumber not in AXIS_DEVICES:
config_file = _read_config(hass) config_file = _read_config(hass)
if serialnumber in config_file: if serialnumber in config_file:
# Device config saved to file
try: try:
config = DEVICE_SCHEMA(config_file[serialnumber]) config = DEVICE_SCHEMA(config_file[serialnumber])
config[CONF_HOST] = host
except vol.Invalid as err: except vol.Invalid as err:
_LOGGER.error("Bad data from %s. %s", CONFIG_FILE, err) _LOGGER.error("Bad data from %s. %s", CONFIG_FILE, err)
return False return False
if not setup_device(hass, config): if not setup_device(hass, config):
_LOGGER.error("Couldn\'t set up %s", config['name']) _LOGGER.error("Couldn\'t set up %s", config[CONF_NAME])
else: else:
# New device, create configuration request for UI
request_configuration(hass, name, host, serialnumber) request_configuration(hass, name, host, serialnumber)
else:
# Device already registered, but on a different IP
device = AXIS_DEVICES[serialnumber]
device.url = host
async_dispatcher_send(hass,
DOMAIN + '_' + device.name + '_new_ip',
host)
# Register discovery service
discovery.listen(hass, SERVICE_AXIS, axis_device_discovered) discovery.listen(hass, SERVICE_AXIS, axis_device_discovered)
if DOMAIN in base_config: if DOMAIN in base_config:
@ -160,7 +188,30 @@ def setup(hass, base_config):
if CONF_NAME not in config: if CONF_NAME not in config:
config[CONF_NAME] = device config[CONF_NAME] = device
if not setup_device(hass, config): if not setup_device(hass, config):
_LOGGER.error("Couldn\'t set up %s", config['name']) _LOGGER.error("Couldn\'t set up %s", config[CONF_NAME])
# Services to communicate with device.
descriptions = load_yaml_config_file(
os.path.join(os.path.dirname(__file__), 'services.yaml'))
def vapix_service(call):
"""Service to send a message."""
for _, device in AXIS_DEVICES.items():
if device.name == call.data[CONF_NAME]:
response = device.do_request(call.data[SERVICE_CGI],
call.data[SERVICE_ACTION],
call.data[SERVICE_PARAM])
hass.bus.async_fire(SERVICE_VAPIX_CALL_RESPONSE, response)
return True
_LOGGER.info("Couldn\'t find device %s", call.data[CONF_NAME])
return False
# Register service with Home Assistant.
hass.services.register(DOMAIN,
SERVICE_VAPIX_CALL,
vapix_service,
descriptions[DOMAIN][SERVICE_VAPIX_CALL],
schema=SERVICE_SCHEMA)
return True return True
@ -190,8 +241,16 @@ def setup_device(hass, config):
if enable_metadatastream: if enable_metadatastream:
device.initialize_new_event = event_initialized device.initialize_new_event = event_initialized
device.initiate_metadatastream() if not device.initiate_metadatastream():
notification = get_component('persistent_notification')
notification.create(hass,
'Dependency missing for sensors, '
'please check documentation',
title=DOMAIN,
notification_id='axis_notification')
AXIS_DEVICES[device.serial_number] = device AXIS_DEVICES[device.serial_number] = device
return True return True
@ -311,4 +370,4 @@ REMAP = [{'type': 'motion',
'class': 'input', 'class': 'input',
'topic': 'tns1:Device/tnsaxis:IO/Port', 'topic': 'tns1:Device/tnsaxis:IO/Port',
'subscribe': 'onvif:Device/axis:IO/Port', 'subscribe': 'onvif:Device/axis:IO/Port',
'platform': 'sensor'}, ] 'platform': 'binary_sensor'}, ]

View File

@ -51,7 +51,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
add_devices([ArestBinarySensor( add_devices([ArestBinarySensor(
arest, resource, config.get(CONF_NAME, response[CONF_NAME]), arest, resource, config.get(CONF_NAME, response[CONF_NAME]),
device_class, pin)]) device_class, pin)], True)
class ArestBinarySensor(BinarySensorDevice): class ArestBinarySensor(BinarySensorDevice):
@ -64,7 +64,6 @@ class ArestBinarySensor(BinarySensorDevice):
self._name = name self._name = name
self._device_class = device_class self._device_class = device_class
self._pin = pin self._pin = pin
self.update()
if self._pin is not None: if self._pin is not None:
request = requests.get( request = requests.get(

View File

@ -8,19 +8,18 @@ import logging
import voluptuous as vol import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.binary_sensor import ( from homeassistant.components.binary_sensor import (
BinarySensorDevice, PLATFORM_SCHEMA) BinarySensorDevice, PLATFORM_SCHEMA)
from homeassistant.components.digital_ocean import ( from homeassistant.components.digital_ocean import (
CONF_DROPLETS, ATTR_CREATED_AT, ATTR_DROPLET_ID, ATTR_DROPLET_NAME, CONF_DROPLETS, ATTR_CREATED_AT, ATTR_DROPLET_ID, ATTR_DROPLET_NAME,
ATTR_FEATURES, ATTR_IPV4_ADDRESS, ATTR_IPV6_ADDRESS, ATTR_MEMORY, ATTR_FEATURES, ATTR_IPV4_ADDRESS, ATTR_IPV6_ADDRESS, ATTR_MEMORY,
ATTR_REGION, ATTR_VCPUS) ATTR_REGION, ATTR_VCPUS, DATA_DIGITAL_OCEAN)
from homeassistant.loader import get_component
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = 'Droplet' DEFAULT_NAME = 'Droplet'
DEFAULT_SENSOR_CLASS = 'motion' DEFAULT_SENSOR_CLASS = 'moving'
DEPENDENCIES = ['digital_ocean'] DEPENDENCIES = ['digital_ocean']
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
@ -30,19 +29,21 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Digital Ocean droplet sensor.""" """Set up the Digital Ocean droplet sensor."""
digital_ocean = get_component('digital_ocean') digital = hass.data.get(DATA_DIGITAL_OCEAN)
if not digital:
return False
droplets = config.get(CONF_DROPLETS) droplets = config.get(CONF_DROPLETS)
dev = [] dev = []
for droplet in droplets: for droplet in droplets:
droplet_id = digital_ocean.DIGITAL_OCEAN.get_droplet_id(droplet) droplet_id = digital.get_droplet_id(droplet)
if droplet_id is None: if droplet_id is None:
_LOGGER.error("Droplet %s is not available", droplet) _LOGGER.error("Droplet %s is not available", droplet)
return False return False
dev.append(DigitalOceanBinarySensor( dev.append(DigitalOceanBinarySensor(digital, droplet_id))
digital_ocean.DIGITAL_OCEAN, droplet_id))
add_devices(dev) add_devices(dev, True)
class DigitalOceanBinarySensor(BinarySensorDevice): class DigitalOceanBinarySensor(BinarySensorDevice):
@ -53,7 +54,7 @@ class DigitalOceanBinarySensor(BinarySensorDevice):
self._digital_ocean = do self._digital_ocean = do
self._droplet_id = droplet_id self._droplet_id = droplet_id
self._state = None self._state = None
self.update() self.data = None
@property @property
def name(self): def name(self):

View File

@ -50,6 +50,10 @@ class ModbusCoilSensor(BinarySensorDevice):
self._coil = int(coil) self._coil = int(coil)
self._value = None self._value = None
def name(self):
"""Return the name of the sensor."""
return self._name
@property @property
def is_on(self): def is_on(self):
"""Return the state of the sensor.""" """Return the state of the sensor."""

View File

@ -0,0 +1,233 @@
"""
Support for RFXtrx binary sensors.
Lighting4 devices (sensors based on PT2262 encoder) are supported and
tested. Other types may need some work.
"""
import logging
import voluptuous as vol
from homeassistant.components import rfxtrx
from homeassistant.util import slugify
from homeassistant.util import dt as dt_util
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers import event as evt
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.rfxtrx import (
ATTR_AUTOMATIC_ADD, ATTR_NAME, ATTR_OFF_DELAY, ATTR_FIREEVENT,
ATTR_DATA_BITS, CONF_DEVICES
)
from homeassistant.const import (
CONF_DEVICE_CLASS, CONF_COMMAND_ON, CONF_COMMAND_OFF
)
DEPENDENCIES = ["rfxtrx"]
_LOGGER = logging.getLogger(__name__)
PLATFORM_SCHEMA = vol.Schema({
vol.Required("platform"): rfxtrx.DOMAIN,
vol.Optional(CONF_DEVICES, default={}): vol.All(
dict, rfxtrx.valid_binary_sensor),
vol.Optional(ATTR_AUTOMATIC_ADD, default=False): cv.boolean,
}, extra=vol.ALLOW_EXTRA)
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
"""Setup the Binary Sensor platform to rfxtrx."""
import RFXtrx as rfxtrxmod
sensors = []
for packet_id, entity in config['devices'].items():
event = rfxtrx.get_rfx_object(packet_id)
device_id = slugify(event.device.id_string.lower())
if device_id in rfxtrx.RFX_DEVICES:
continue
if entity[ATTR_DATA_BITS] is not None:
_LOGGER.info("Masked device id: %s",
rfxtrx.get_pt2262_deviceid(device_id,
entity[ATTR_DATA_BITS]))
_LOGGER.info("Add %s rfxtrx.binary_sensor (class %s)",
entity[ATTR_NAME], entity[CONF_DEVICE_CLASS])
device = RfxtrxBinarySensor(event, entity[ATTR_NAME],
entity[CONF_DEVICE_CLASS],
entity[ATTR_FIREEVENT],
entity[ATTR_OFF_DELAY],
entity[ATTR_DATA_BITS],
entity[CONF_COMMAND_ON],
entity[CONF_COMMAND_OFF])
device.hass = hass
sensors.append(device)
rfxtrx.RFX_DEVICES[device_id] = device
add_devices_callback(sensors)
# pylint: disable=too-many-branches
def binary_sensor_update(event):
"""Callback for control updates from the RFXtrx gateway."""
if not isinstance(event, rfxtrxmod.ControlEvent):
return
device_id = slugify(event.device.id_string.lower())
if device_id in rfxtrx.RFX_DEVICES:
sensor = rfxtrx.RFX_DEVICES[device_id]
else:
sensor = rfxtrx.get_pt2262_device(device_id)
if sensor is None:
# Add the entity if not exists and automatic_add is True
if not config[ATTR_AUTOMATIC_ADD]:
return
poss_dev = rfxtrx.find_possible_pt2262_device(device_id)
if poss_dev is not None:
poss_id = slugify(poss_dev.event.device.id_string.lower())
_LOGGER.info("Found possible matching deviceid %s.",
poss_id)
pkt_id = "".join("{0:02x}".format(x) for x in event.data)
sensor = RfxtrxBinarySensor(event, pkt_id)
rfxtrx.RFX_DEVICES[device_id] = sensor
add_devices_callback([sensor])
_LOGGER.info("Added binary sensor %s "
"(Device_id: %s Class: %s Sub: %s)",
pkt_id,
slugify(event.device.id_string.lower()),
event.device.__class__.__name__,
event.device.subtype)
elif not isinstance(sensor, RfxtrxBinarySensor):
return
else:
_LOGGER.info("Binary sensor update "
"(Device_id: %s Class: %s Sub: %s)",
slugify(event.device.id_string.lower()),
event.device.__class__.__name__,
event.device.subtype)
if sensor.is_pt2262:
cmd = rfxtrx.get_pt2262_cmd(device_id, sensor.data_bits)
_LOGGER.info("applying cmd %s to device_id: %s)",
cmd, sensor.masked_id)
sensor.apply_cmd(int(cmd, 16))
else:
rfxtrx.apply_received_command(event)
if (sensor.is_on and sensor.off_delay is not None and
sensor.delay_listener is None):
def off_delay_listener(now):
"""Switch device off after a delay."""
sensor.delay_listener = None
sensor.update_state(False)
sensor.delay_listener = evt.track_point_in_time(
hass, off_delay_listener, dt_util.utcnow() + sensor.off_delay
)
# Subscribe to main rfxtrx events
if binary_sensor_update not in rfxtrx.RECEIVED_EVT_SUBSCRIBERS:
rfxtrx.RECEIVED_EVT_SUBSCRIBERS.append(binary_sensor_update)
# pylint: disable=too-many-instance-attributes,too-many-arguments
class RfxtrxBinarySensor(BinarySensorDevice):
"""An Rfxtrx binary sensor."""
def __init__(self, event, name, device_class=None,
should_fire=False, off_delay=None, data_bits=None,
cmd_on=None, cmd_off=None):
"""Initialize the sensor."""
self.event = event
self._name = name
self._should_fire_event = should_fire
self._device_class = device_class
self._off_delay = off_delay
self._state = False
self.delay_listener = None
self._data_bits = data_bits
self._cmd_on = cmd_on
self._cmd_off = cmd_off
if data_bits is not None:
self._masked_id = rfxtrx.get_pt2262_deviceid(
event.device.id_string.lower(),
data_bits)
def __str__(self):
"""Return the name of the sensor."""
return self._name
@property
def name(self):
"""Return the device name."""
return self._name
@property
def is_pt2262(self):
"""Return true if the device is PT2262-based."""
return self._data_bits is not None
@property
def masked_id(self):
"""Return the masked device id (isolated address bits)."""
return self._masked_id
@property
def data_bits(self):
"""Return the number of data bits."""
return self._data_bits
@property
def cmd_on(self):
"""Return the value of the 'On' command."""
return self._cmd_on
@property
def cmd_off(self):
"""Return the value of the 'Off' command."""
return self._cmd_off
@property
def should_poll(self):
"""No polling needed."""
return False
@property
def should_fire_event(self):
"""Return is the device must fire event."""
return self._should_fire_event
@property
def device_class(self):
"""Return the sensor class."""
return self._device_class
@property
def off_delay(self):
"""Return the off_delay attribute value."""
return self._off_delay
@property
def is_on(self):
"""Return true if the sensor state is True."""
return self._state
def apply_cmd(self, cmd):
"""Apply a command for updating the state."""
if cmd == self.cmd_on:
self.update_state(True)
elif cmd == self.cmd_off:
self.update_state(False)
def update_state(self, state):
"""Update the state of the device."""
self._state = state
self.schedule_update_ha_state()

View File

@ -0,0 +1,59 @@
"""
Interfaces with Verisure sensors.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.verisure/
"""
import logging
from homeassistant.components.verisure import HUB as hub
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.verisure import CONF_DOOR_WINDOW
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup Verisure binary sensors."""
sensors = []
hub.update_overview()
if int(hub.config.get(CONF_DOOR_WINDOW, 1)):
sensors.extend([
VerisureDoorWindowSensor(device_label)
for device_label in hub.get(
"$.doorWindow.doorWindowDevice[*].deviceLabel")])
add_devices(sensors)
class VerisureDoorWindowSensor(BinarySensorDevice):
"""Verisure door window sensor."""
def __init__(self, device_label):
"""Initialize the modbus coil sensor."""
self._device_label = device_label
@property
def name(self):
"""Return the name of the binary sensor."""
return hub.get_first(
"$.doorWindow.doorWindowDevice[?(@.deviceLabel=='%s')].area",
self._device_label)
@property
def is_on(self):
"""Return the state of the sensor."""
return hub.get_first(
"$.doorWindow.doorWindowDevice[?(@.deviceLabel=='%s')].state",
self._device_label) == "OPEN"
@property
def available(self):
"""Return True if entity is available."""
return hub.get_first(
"$.doorWindow.doorWindowDevice[?(@.deviceLabel=='%s')]",
self._device_label) is not None
def update(self):
"""Update the state of the sensor."""
hub.update_overview()

View File

@ -30,7 +30,7 @@ def setup_platform(hass, config, add_devices, disc_info=None):
if disc_info is None: if disc_info is None:
return return
if not any([data[CONF_TRACK] for data in disc_info[CONF_ENTITIES]]): if not any(data[CONF_TRACK] for data in disc_info[CONF_ENTITIES]):
return return
calendar_service = GoogleCalendarService(hass.config.path(TOKEN_FILE)) calendar_service = GoogleCalendarService(hass.config.path(TOKEN_FILE))

View File

@ -12,13 +12,16 @@ from datetime import timedelta
import logging import logging
import hashlib import hashlib
from random import SystemRandom from random import SystemRandom
import os
import aiohttp import aiohttp
from aiohttp import web from aiohttp import web
import async_timeout import async_timeout
import voluptuous as vol
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.const import ATTR_ENTITY_PICTURE from homeassistant.const import (ATTR_ENTITY_ID, ATTR_ENTITY_PICTURE)
from homeassistant.config import load_yaml_config_file
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
@ -26,9 +29,12 @@ from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
from homeassistant.components.http import HomeAssistantView, KEY_AUTHENTICATED from homeassistant.components.http import HomeAssistantView, KEY_AUTHENTICATED
from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.event import async_track_time_interval
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
SERVICE_EN_MOTION = 'enable_motion_detection'
SERVICE_DISEN_MOTION = 'disable_motion_detection'
DOMAIN = 'camera' DOMAIN = 'camera'
DEPENDENCIES = ['http'] DEPENDENCIES = ['http']
SCAN_INTERVAL = timedelta(seconds=30) SCAN_INTERVAL = timedelta(seconds=30)
@ -38,11 +44,30 @@ STATE_RECORDING = 'recording'
STATE_STREAMING = 'streaming' STATE_STREAMING = 'streaming'
STATE_IDLE = 'idle' STATE_IDLE = 'idle'
DEFAULT_CONTENT_TYPE = 'image/jpeg'
ENTITY_IMAGE_URL = '/api/camera_proxy/{0}?token={1}' ENTITY_IMAGE_URL = '/api/camera_proxy/{0}?token={1}'
TOKEN_CHANGE_INTERVAL = timedelta(minutes=5) TOKEN_CHANGE_INTERVAL = timedelta(minutes=5)
_RND = SystemRandom() _RND = SystemRandom()
CAMERA_SERVICE_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
})
def enable_motion_detection(hass, entity_id=None):
"""Enable Motion Detection."""
data = {ATTR_ENTITY_ID: entity_id} if entity_id else None
hass.async_add_job(hass.services.async_call(
DOMAIN, SERVICE_EN_MOTION, data))
def disable_motion_detection(hass, entity_id=None):
"""Disable Motion Detection."""
data = {ATTR_ENTITY_ID: entity_id} if entity_id else None
hass.async_add_job(hass.services.async_call(
DOMAIN, SERVICE_DISEN_MOTION, data))
@asyncio.coroutine @asyncio.coroutine
def async_get_image(hass, entity_id, timeout=10): def async_get_image(hass, entity_id, timeout=10):
@ -92,6 +117,44 @@ def async_setup(hass, config):
hass.async_add_job(entity.async_update_ha_state()) hass.async_add_job(entity.async_update_ha_state())
async_track_time_interval(hass, update_tokens, TOKEN_CHANGE_INTERVAL) async_track_time_interval(hass, update_tokens, TOKEN_CHANGE_INTERVAL)
@asyncio.coroutine
def async_handle_camera_service(service):
"""Handle calls to the camera services."""
target_cameras = component.async_extract_from_service(service)
for camera in target_cameras:
if service.service == SERVICE_EN_MOTION:
yield from camera.async_enable_motion_detection()
elif service.service == SERVICE_DISEN_MOTION:
yield from camera.async_disable_motion_detection()
update_tasks = []
for camera in target_cameras:
if not camera.should_poll:
continue
update_coro = hass.async_add_job(
camera.async_update_ha_state(True))
if hasattr(camera, 'async_update'):
update_tasks.append(update_coro)
else:
yield from update_coro
if update_tasks:
yield from asyncio.wait(update_tasks, loop=hass.loop)
descriptions = yield from hass.async_add_job(
load_yaml_config_file, os.path.join(
os.path.dirname(__file__), 'services.yaml'))
hass.services.async_register(
DOMAIN, SERVICE_EN_MOTION, async_handle_camera_service,
descriptions.get(SERVICE_EN_MOTION), schema=CAMERA_SERVICE_SCHEMA)
hass.services.async_register(
DOMAIN, SERVICE_DISEN_MOTION, async_handle_camera_service,
descriptions.get(SERVICE_DISEN_MOTION), schema=CAMERA_SERVICE_SCHEMA)
return True return True
@ -101,6 +164,7 @@ class Camera(Entity):
def __init__(self): def __init__(self):
"""Initialize a camera.""" """Initialize a camera."""
self.is_streaming = False self.is_streaming = False
self.content_type = DEFAULT_CONTENT_TYPE
self.access_tokens = collections.deque([], 2) self.access_tokens = collections.deque([], 2)
self.async_update_token() self.async_update_token()
@ -124,6 +188,11 @@ class Camera(Entity):
"""Return the camera brand.""" """Return the camera brand."""
return None return None
@property
def motion_detection_enabled(self):
"""Return the camera motion detection status."""
return None
@property @property
def model(self): def model(self):
"""Return the camera model.""" """Return the camera model."""
@ -149,16 +218,17 @@ class Camera(Entity):
response = web.StreamResponse() response = web.StreamResponse()
response.content_type = ('multipart/x-mixed-replace; ' response.content_type = ('multipart/x-mixed-replace; '
'boundary=--jpegboundary') 'boundary=--frameboundary')
yield from response.prepare(request) yield from response.prepare(request)
def write(img_bytes): def write(img_bytes):
"""Write image to stream.""" """Write image to stream."""
response.write(bytes( response.write(bytes(
'--jpegboundary\r\n' '--frameboundary\r\n'
'Content-Type: image/jpeg\r\n' 'Content-Type: {}\r\n'
'Content-Length: {}\r\n\r\n'.format( 'Content-Length: {}\r\n\r\n'.format(
len(img_bytes)), 'utf-8') + img_bytes + b'\r\n') self.content_type, len(img_bytes)),
'utf-8') + img_bytes + b'\r\n')
last_image = None last_image = None
@ -199,6 +269,22 @@ class Camera(Entity):
else: else:
return STATE_IDLE return STATE_IDLE
def enable_motion_detection(self):
"""Enable motion detection in the camera."""
raise NotImplementedError()
def async_enable_motion_detection(self):
"""Call the job and enable motion detection."""
return self.hass.async_add_job(self.enable_motion_detection)
def disable_motion_detection(self):
"""Disable motion detection in camera."""
raise NotImplementedError()
def async_disable_motion_detection(self):
"""Call the job and disable motion detection."""
return self.hass.async_add_job(self.disable_motion_detection)
@property @property
def state_attributes(self): def state_attributes(self):
"""Return the camera state attributes.""" """Return the camera state attributes."""
@ -212,6 +298,9 @@ class Camera(Entity):
if self.brand: if self.brand:
attr['brand'] = self.brand attr['brand'] = self.brand
if self.motion_detection_enabled:
attr['motion_detection'] = self.motion_detection_enabled
return attr return attr
@callback @callback
@ -269,7 +358,8 @@ class CameraImageView(CameraView):
image = yield from camera.async_camera_image() image = yield from camera.async_camera_image()
if image: if image:
return web.Response(body=image, content_type='image/jpeg') return web.Response(body=image,
content_type=camera.content_type)
return web.Response(status=500) return web.Response(status=500)

View File

@ -6,32 +6,32 @@ https://home-assistant.io/components/camera.arlo/
""" """
import asyncio import asyncio
import logging import logging
import voluptuous as vol import voluptuous as vol
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.components.arlo import DEFAULT_BRAND from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream
from homeassistant.components.arlo import DEFAULT_BRAND, DATA_ARLO
from homeassistant.components.camera import (Camera, PLATFORM_SCHEMA) from homeassistant.components.camera import Camera, PLATFORM_SCHEMA
from homeassistant.components.ffmpeg import DATA_FFMPEG from homeassistant.components.ffmpeg import DATA_FFMPEG
from homeassistant.helpers.aiohttp_client import (
async_aiohttp_proxy_stream)
DEPENDENCIES = ['arlo', 'ffmpeg'] DEPENDENCIES = ['arlo', 'ffmpeg']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONF_FFMPEG_ARGUMENTS = 'ffmpeg_arguments' CONF_FFMPEG_ARGUMENTS = 'ffmpeg_arguments'
ARLO_MODE_ARMED = 'armed'
ARLO_MODE_DISARMED = 'disarmed'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_FFMPEG_ARGUMENTS): vol.Optional(CONF_FFMPEG_ARGUMENTS): cv.string,
cv.string,
}) })
@asyncio.coroutine @asyncio.coroutine
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 an Arlo IP Camera.""" """Set up an Arlo IP Camera."""
arlo = hass.data.get('arlo') arlo = hass.data.get(DATA_ARLO)
if not arlo: if not arlo:
return False return False
@ -40,7 +40,6 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
cameras.append(ArloCam(hass, camera, config)) cameras.append(ArloCam(hass, camera, config))
async_add_devices(cameras, True) async_add_devices(cameras, True)
return True
class ArloCam(Camera): class ArloCam(Camera):
@ -49,14 +48,15 @@ class ArloCam(Camera):
def __init__(self, hass, camera, device_info): def __init__(self, hass, camera, device_info):
"""Initialize an Arlo camera.""" """Initialize an Arlo camera."""
super().__init__() super().__init__()
self._camera = camera self._camera = camera
self._base_stn = hass.data['arlo'].base_stations[0]
self._name = self._camera.name self._name = self._camera.name
self._motion_status = False
self._ffmpeg = hass.data[DATA_FFMPEG] self._ffmpeg = hass.data[DATA_FFMPEG]
self._ffmpeg_arguments = device_info.get(CONF_FFMPEG_ARGUMENTS) self._ffmpeg_arguments = device_info.get(CONF_FFMPEG_ARGUMENTS)
def camera_image(self): def camera_image(self):
"""Return a still image reponse from the camera.""" """Return a still image response from the camera."""
return self._camera.last_image return self._camera.last_image
@asyncio.coroutine @asyncio.coroutine
@ -90,3 +90,27 @@ class ArloCam(Camera):
def brand(self): def brand(self):
"""Camera brand.""" """Camera brand."""
return DEFAULT_BRAND return DEFAULT_BRAND
@property
def should_poll(self):
"""Camera should poll periodically."""
return True
@property
def motion_detection_enabled(self):
"""Camera Motion Detection Status."""
return self._motion_status
def set_base_station_mode(self, mode):
"""Set the mode in the base station."""
self._base_stn.mode = mode
def enable_motion_detection(self):
"""Enable the Motion detection in base station (Arm)."""
self._motion_status = True
self.set_base_station_mode(ARLO_MODE_ARMED)
def disable_motion_detection(self):
"""Disable the motion detection in base station (Disarm)."""
self._motion_status = False
self.set_base_station_mode(ARLO_MODE_DISARMED)

View File

@ -7,15 +7,16 @@ https://home-assistant.io/components/camera.axis/
import logging import logging
from homeassistant.const import ( from homeassistant.const import (
CONF_NAME, CONF_USERNAME, CONF_PASSWORD, CONF_HOST, CONF_NAME, CONF_USERNAME, CONF_PASSWORD,
CONF_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION) CONF_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION)
from homeassistant.components.camera.mjpeg import ( from homeassistant.components.camera.mjpeg import (
CONF_MJPEG_URL, CONF_STILL_IMAGE_URL, MjpegCamera) CONF_MJPEG_URL, CONF_STILL_IMAGE_URL, MjpegCamera)
from homeassistant.helpers.dispatcher import async_dispatcher_connect
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['axis']
DOMAIN = 'axis' DOMAIN = 'axis'
DEPENDENCIES = [DOMAIN]
def _get_image_url(host, mode): def _get_image_url(host, mode):
@ -27,12 +28,29 @@ def _get_image_url(host, mode):
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup Axis camera.""" """Setup Axis camera."""
device_info = { config = {
CONF_NAME: discovery_info['name'], CONF_NAME: discovery_info[CONF_NAME],
CONF_USERNAME: discovery_info['username'], CONF_USERNAME: discovery_info[CONF_USERNAME],
CONF_PASSWORD: discovery_info['password'], CONF_PASSWORD: discovery_info[CONF_PASSWORD],
CONF_MJPEG_URL: _get_image_url(discovery_info['host'], 'mjpeg'), CONF_MJPEG_URL: _get_image_url(discovery_info[CONF_HOST], 'mjpeg'),
CONF_STILL_IMAGE_URL: _get_image_url(discovery_info['host'], 'single'), CONF_STILL_IMAGE_URL: _get_image_url(discovery_info[CONF_HOST],
'single'),
CONF_AUTHENTICATION: HTTP_DIGEST_AUTHENTICATION, CONF_AUTHENTICATION: HTTP_DIGEST_AUTHENTICATION,
} }
add_devices([MjpegCamera(hass, device_info)]) add_devices([AxisCamera(hass, config)])
class AxisCamera(MjpegCamera):
"""AxisCamera class."""
def __init__(self, hass, config):
"""Initialize Axis Communications camera component."""
super().__init__(hass, config)
async_dispatcher_connect(hass,
DOMAIN + '_' + config[CONF_NAME] + '_new_ip',
self._new_ip)
def _new_ip(self, host):
"""Set new IP for video stream."""
self._mjpeg_url = _get_image_url(host, 'mjpeg')
self._still_image_url = _get_image_url(host, 'mjpeg')

View File

@ -5,25 +5,29 @@ For more details about this platform, please refer to the documentation
https://home-assistant.io/components/demo/ https://home-assistant.io/components/demo/
""" """
import os import os
import logging
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
from homeassistant.components.camera import Camera from homeassistant.components.camera import Camera
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Demo camera platform.""" """Set up the Demo camera platform."""
add_devices([ add_devices([
DemoCamera('Demo camera') DemoCamera(hass, config, 'Demo camera')
]) ])
class DemoCamera(Camera): class DemoCamera(Camera):
"""The representation of a Demo camera.""" """The representation of a Demo camera."""
def __init__(self, name): def __init__(self, hass, config, name):
"""Initialize demo camera component.""" """Initialize demo camera component."""
super().__init__() super().__init__()
self._parent = hass
self._name = name self._name = name
self._motion_status = False
def camera_image(self): def camera_image(self):
"""Return a faked still image response.""" """Return a faked still image response."""
@ -38,3 +42,21 @@ class DemoCamera(Camera):
def name(self): def name(self):
"""Return the name of this camera.""" """Return the name of this camera."""
return self._name return self._name
@property
def should_poll(self):
"""Camera should poll periodically."""
return True
@property
def motion_detection_enabled(self):
"""Camera Motion Detection Status."""
return self._motion_status
def enable_motion_detection(self):
"""Enable the Motion detection in base station (Arm)."""
self._motion_status = True
def disable_motion_detection(self):
"""Disable the motion detection in base station (Disarm)."""
self._motion_status = False

View File

@ -17,13 +17,15 @@ from homeassistant.const import (
CONF_NAME, CONF_USERNAME, CONF_PASSWORD, CONF_AUTHENTICATION, CONF_NAME, CONF_USERNAME, CONF_PASSWORD, CONF_AUTHENTICATION,
HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION) HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION)
from homeassistant.exceptions import TemplateError from homeassistant.exceptions import TemplateError
from homeassistant.components.camera import (PLATFORM_SCHEMA, Camera) from homeassistant.components.camera import (
PLATFORM_SCHEMA, DEFAULT_CONTENT_TYPE, Camera)
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.util.async import run_coroutine_threadsafe from homeassistant.util.async import run_coroutine_threadsafe
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONF_CONTENT_TYPE = 'content_type'
CONF_LIMIT_REFETCH_TO_URL_CHANGE = 'limit_refetch_to_url_change' CONF_LIMIT_REFETCH_TO_URL_CHANGE = 'limit_refetch_to_url_change'
CONF_STILL_IMAGE_URL = 'still_image_url' CONF_STILL_IMAGE_URL = 'still_image_url'
@ -37,6 +39,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_PASSWORD): cv.string, vol.Optional(CONF_PASSWORD): cv.string,
vol.Optional(CONF_USERNAME): cv.string, vol.Optional(CONF_USERNAME): cv.string,
vol.Optional(CONF_CONTENT_TYPE, default=DEFAULT_CONTENT_TYPE): cv.string,
}) })
@ -59,6 +62,7 @@ class GenericCamera(Camera):
self._still_image_url = device_info[CONF_STILL_IMAGE_URL] self._still_image_url = device_info[CONF_STILL_IMAGE_URL]
self._still_image_url.hass = hass self._still_image_url.hass = hass
self._limit_refetch = device_info[CONF_LIMIT_REFETCH_TO_URL_CHANGE] self._limit_refetch = device_info[CONF_LIMIT_REFETCH_TO_URL_CHANGE]
self.content_type = device_info[CONF_CONTENT_TYPE]
username = device_info.get(CONF_USERNAME) username = device_info.get(CONF_USERNAME)
password = device_info.get(CONF_PASSWORD) password = device_info.get(CONF_PASSWORD)

View File

@ -5,6 +5,7 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/camera.local_file/ https://home-assistant.io/components/camera.local_file/
""" """
import logging import logging
import mimetypes
import os import os
import voluptuous as vol import voluptuous as vol
@ -46,6 +47,10 @@ class LocalFile(Camera):
self._name = name self._name = name
self._file_path = file_path self._file_path = file_path
# Set content type of local file
content, _ = mimetypes.guess_type(file_path)
if content is not None:
self.content_type = content
def camera_image(self): def camera_image(self):
"""Return image response.""" """Return image response."""

View File

@ -0,0 +1,17 @@
# Describes the format for available camera services
enable_motion_detection:
description: Enable the motion detection in a camera
fields:
entity_id:
description: Name(s) of entities to enable motion detection
example: 'camera.living_room_camera'
disable_motion_detection:
description: Disable the motion detection in a camera
fields:
entity_id:
description: Name(s) of entities to disable motion detection
example: 'camera.living_room_camera'

View File

@ -24,22 +24,23 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
if not os.access(directory_path, os.R_OK): if not os.access(directory_path, os.R_OK):
_LOGGER.error("file path %s is not readable", directory_path) _LOGGER.error("file path %s is not readable", directory_path)
return False return False
hub.update_smartcam() hub.update_overview()
smartcams = [] smartcams = []
smartcams.extend([ smartcams.extend([
VerisureSmartcam(hass, value.deviceLabel, directory_path) VerisureSmartcam(hass, device_label, directory_path)
for value in hub.smartcam_status.values()]) for device_label in hub.get(
"$.customerImageCameras[*].deviceLabel")])
add_devices(smartcams) add_devices(smartcams)
class VerisureSmartcam(Camera): class VerisureSmartcam(Camera):
"""Representation of a Verisure camera.""" """Representation of a Verisure camera."""
def __init__(self, hass, device_id, directory_path): def __init__(self, hass, device_label, directory_path):
"""Initialize Verisure File Camera component.""" """Initialize Verisure File Camera component."""
super().__init__() super().__init__()
self._device_id = device_id self._device_label = device_label
self._directory_path = directory_path self._directory_path = directory_path
self._image = None self._image = None
self._image_id = None self._image_id = None
@ -58,28 +59,27 @@ class VerisureSmartcam(Camera):
def check_imagelist(self): def check_imagelist(self):
"""Check the contents of the image list.""" """Check the contents of the image list."""
hub.update_smartcam_imagelist() hub.update_smartcam_imageseries()
if (self._device_id not in hub.smartcam_dict or image_ids = hub.get_image_info(
not hub.smartcam_dict[self._device_id]): "$.imageSeries[?(@.deviceLabel=='%s')].image[0].imageId",
self._device_label)
if not image_ids:
return return
images = hub.smartcam_dict[self._device_id] new_image_id = image_ids[0]
new_image_id = images[0]
_LOGGER.debug("self._device_id=%s, self._images=%s, "
"self._new_image_id=%s", self._device_id,
images, new_image_id)
if (new_image_id == '-1' or if (new_image_id == '-1' or
self._image_id == new_image_id): self._image_id == new_image_id):
_LOGGER.debug("The image is the same, or loading image_id") _LOGGER.debug("The image is the same, or loading image_id")
return return
_LOGGER.debug("Download new image %s", new_image_id) _LOGGER.debug("Download new image %s", new_image_id)
hub.my_pages.smartcam.download_image( new_image_path = os.path.join(
self._device_id, new_image_id, self._directory_path) self._directory_path, '{}{}'.format(new_image_id, '.jpg'))
hub.session.download_image(
self._device_label, new_image_id, new_image_path)
_LOGGER.debug("Old image_id=%s", self._image_id) _LOGGER.debug("Old image_id=%s", self._image_id)
self.delete_image(self) self.delete_image(self)
self._image_id = new_image_id self._image_id = new_image_id
self._image = os.path.join( self._image = new_image_path
self._directory_path, '{}{}'.format(self._image_id, '.jpg'))
def delete_image(self, event): def delete_image(self, event):
"""Delete an old image.""" """Delete an old image."""
@ -95,4 +95,6 @@ class VerisureSmartcam(Camera):
@property @property
def name(self): def name(self):
"""Return the name of this camera.""" """Return the name of this camera."""
return hub.smartcam_status[self._device_id].location return hub.get_first(
"$.customerImageCameras[?(@.deviceLabel=='%s')].area",
self._device_label)

View File

@ -693,8 +693,14 @@ class ClimateDevice(Entity):
def _convert_for_display(self, temp): def _convert_for_display(self, temp):
"""Convert temperature into preferred units for display purposes.""" """Convert temperature into preferred units for display purposes."""
if temp is None or not isinstance(temp, Number): if temp is None:
return temp return temp
# if the temperature is not a number this can cause issues
# with polymer components, so bail early there.
if not isinstance(temp, Number):
raise TypeError("Temperature is not a number: %s" % temp)
if self.temperature_unit != self.unit_of_measurement: if self.temperature_unit != self.unit_of_measurement:
temp = convert_temperature( temp = convert_temperature(
temp, self.temperature_unit, self.unit_of_measurement) temp, self.temperature_unit, self.unit_of_measurement)

View File

@ -67,7 +67,12 @@ class MySensorsHVAC(mysensors.MySensorsDeviceEntity, ClimateDevice):
@property @property
def current_temperature(self): def current_temperature(self):
"""Return the current temperature.""" """Return the current temperature."""
return self._values.get(self.gateway.const.SetReq.V_TEMP) value = self._values.get(self.gateway.const.SetReq.V_TEMP)
if value is not None:
value = float(value)
return value
@property @property
def target_temperature(self): def target_temperature(self):
@ -79,21 +84,21 @@ class MySensorsHVAC(mysensors.MySensorsDeviceEntity, ClimateDevice):
temp = self._values.get(set_req.V_HVAC_SETPOINT_COOL) temp = self._values.get(set_req.V_HVAC_SETPOINT_COOL)
if temp is None: if temp is None:
temp = self._values.get(set_req.V_HVAC_SETPOINT_HEAT) temp = self._values.get(set_req.V_HVAC_SETPOINT_HEAT)
return temp return float(temp)
@property @property
def target_temperature_high(self): def target_temperature_high(self):
"""Return the highbound target temperature we try to reach.""" """Return the highbound target temperature we try to reach."""
set_req = self.gateway.const.SetReq set_req = self.gateway.const.SetReq
if set_req.V_HVAC_SETPOINT_HEAT in self._values: if set_req.V_HVAC_SETPOINT_HEAT in self._values:
return self._values.get(set_req.V_HVAC_SETPOINT_COOL) return float(self._values.get(set_req.V_HVAC_SETPOINT_COOL))
@property @property
def target_temperature_low(self): def target_temperature_low(self):
"""Return the lowbound target temperature we try to reach.""" """Return the lowbound target temperature we try to reach."""
set_req = self.gateway.const.SetReq set_req = self.gateway.const.SetReq
if set_req.V_HVAC_SETPOINT_COOL in self._values: if set_req.V_HVAC_SETPOINT_COOL in self._values:
return self._values.get(set_req.V_HVAC_SETPOINT_HEAT) return float(self._values.get(set_req.V_HVAC_SETPOINT_HEAT))
@property @property
def current_operation(self): def current_operation(self):

View File

@ -15,7 +15,7 @@ from homeassistant.components.climate import (
from homeassistant.const import CONF_HOST, TEMP_FAHRENHEIT, ATTR_TEMPERATURE from homeassistant.const import CONF_HOST, TEMP_FAHRENHEIT, ATTR_TEMPERATURE
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['radiotherm==1.2'] REQUIREMENTS = ['radiotherm==1.3']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -84,6 +84,7 @@ class RadioThermostat(ClimateDevice):
self._name = None self._name = None
self._fmode = None self._fmode = None
self._tmode = None self._tmode = None
self._tstate = None
self._hold_temp = hold_temp self._hold_temp = hold_temp
self._away = False self._away = False
self._away_temps = away_temps self._away_temps = away_temps
@ -140,6 +141,7 @@ class RadioThermostat(ClimateDevice):
self._name = self.device.name['raw'] self._name = self.device.name['raw']
self._fmode = self.device.fmode['human'] self._fmode = self.device.fmode['human']
self._tmode = self.device.tmode['human'] self._tmode = self.device.tmode['human']
self._tstate = self.device.tstate['human']
if self._tmode == 'Cool': if self._tmode == 'Cool':
self._target_temperature = self.device.t_cool['raw'] self._target_temperature = self.device.t_cool['raw']
@ -147,6 +149,12 @@ class RadioThermostat(ClimateDevice):
elif self._tmode == 'Heat': elif self._tmode == 'Heat':
self._target_temperature = self.device.t_heat['raw'] self._target_temperature = self.device.t_heat['raw']
self._current_operation = STATE_HEAT self._current_operation = STATE_HEAT
elif self._tmode == 'Auto':
if self._tstate == 'Cool':
self._target_temperature = self.device.t_cool['raw']
elif self._tstate == 'Heat':
self._target_temperature = self.device.t_heat['raw']
self._current_operation = STATE_AUTO
else: else:
self._current_operation = STATE_IDLE self._current_operation = STATE_IDLE
@ -159,6 +167,12 @@ class RadioThermostat(ClimateDevice):
self.device.t_cool = round(temperature * 2.0) / 2.0 self.device.t_cool = round(temperature * 2.0) / 2.0
elif self._current_operation == STATE_HEAT: elif self._current_operation == STATE_HEAT:
self.device.t_heat = round(temperature * 2.0) / 2.0 self.device.t_heat = round(temperature * 2.0) / 2.0
elif self._current_operation == STATE_AUTO:
if self._tstate == 'Cool':
self.device.t_cool = round(temperature * 2.0) / 2.0
elif self._tstate == 'Heat':
self.device.t_heat = round(temperature * 2.0) / 2.0
if self._hold_temp or self._away: if self._hold_temp or self._away:
self.device.hold = 1 self.device.hold = 1
else: else:

View File

@ -16,6 +16,7 @@ from homeassistant.const import (
ATTR_TEMPERATURE, CONF_API_KEY, CONF_ID, TEMP_CELSIUS, TEMP_FAHRENHEIT) ATTR_TEMPERATURE, CONF_API_KEY, CONF_ID, TEMP_CELSIUS, TEMP_FAHRENHEIT)
from homeassistant.components.climate import ( from homeassistant.components.climate import (
ATTR_CURRENT_HUMIDITY, ClimateDevice, PLATFORM_SCHEMA) ATTR_CURRENT_HUMIDITY, ClimateDevice, PLATFORM_SCHEMA)
from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.util.temperature import convert as convert_temperature from homeassistant.util.temperature import convert as convert_temperature
@ -52,9 +53,10 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
yield from client.async_get_devices(_INITIAL_FETCH_FIELDS)): yield from client.async_get_devices(_INITIAL_FETCH_FIELDS)):
if config[CONF_ID] == ALL or dev['id'] in config[CONF_ID]: if config[CONF_ID] == ALL or dev['id'] in config[CONF_ID]:
devices.append(SensiboClimate(client, dev)) devices.append(SensiboClimate(client, dev))
except aiohttp.client_exceptions.ClientConnectorError: except (aiohttp.client_exceptions.ClientConnectorError,
asyncio.TimeoutError):
_LOGGER.exception('Failed to connct to Sensibo servers.') _LOGGER.exception('Failed to connct to Sensibo servers.')
return False raise PlatformNotReady
if devices: if devices:
async_add_devices(devices) async_add_devices(devices)

View File

@ -24,6 +24,17 @@ CONST_OVERLAY_MANUAL = 'MANUAL'
# the temperature will be reset after a timespan # the temperature will be reset after a timespan
CONST_OVERLAY_TIMER = 'TIMER' CONST_OVERLAY_TIMER = 'TIMER'
CONST_MODE_FAN_HIGH = 'HIGH'
CONST_MODE_FAN_MIDDLE = 'MIDDLE'
CONST_MODE_FAN_LOW = 'LOW'
FAN_MODES_LIST = {
CONST_MODE_FAN_HIGH: 'High',
CONST_MODE_FAN_MIDDLE: 'Middle',
CONST_MODE_FAN_LOW: 'Low',
CONST_MODE_OFF: 'Off',
}
OPERATION_LIST = { OPERATION_LIST = {
CONST_OVERLAY_MANUAL: 'Manual', CONST_OVERLAY_MANUAL: 'Manual',
CONST_OVERLAY_TIMER: 'Timer', CONST_OVERLAY_TIMER: 'Timer',
@ -60,9 +71,15 @@ def create_climate_device(tado, hass, zone, name, zone_id):
capabilities = tado.get_capabilities(zone_id) capabilities = tado.get_capabilities(zone_id)
unit = TEMP_CELSIUS unit = TEMP_CELSIUS
min_temp = float(capabilities['temperatures']['celsius']['min']) ac_mode = capabilities['type'] == 'AIR_CONDITIONING'
max_temp = float(capabilities['temperatures']['celsius']['max'])
ac_mode = capabilities['type'] != 'HEATING' if ac_mode:
temperatures = capabilities['HEAT']['temperatures']
else:
temperatures = capabilities['temperatures']
min_temp = float(temperatures['celsius']['min'])
max_temp = float(temperatures['celsius']['max'])
data_id = 'zone {} {}'.format(name, zone_id) data_id = 'zone {} {}'.format(name, zone_id)
device = TadoClimate(tado, device = TadoClimate(tado,
@ -107,7 +124,9 @@ class TadoClimate(ClimateDevice):
self._max_temp = max_temp self._max_temp = max_temp
self._target_temp = None self._target_temp = None
self._tolerance = tolerance self._tolerance = tolerance
self._cooling = False
self._current_fan = CONST_MODE_OFF
self._current_operation = CONST_MODE_SMART_SCHEDULE self._current_operation = CONST_MODE_SMART_SCHEDULE
self._overlay_mode = CONST_MODE_SMART_SCHEDULE self._overlay_mode = CONST_MODE_SMART_SCHEDULE
@ -129,13 +148,32 @@ class TadoClimate(ClimateDevice):
@property @property
def current_operation(self): def current_operation(self):
"""Return current readable operation mode.""" """Return current readable operation mode."""
return OPERATION_LIST.get(self._current_operation) if self._cooling:
return "Cooling"
else:
return OPERATION_LIST.get(self._current_operation)
@property @property
def operation_list(self): def operation_list(self):
"""Return the list of available operation modes (readable).""" """Return the list of available operation modes (readable)."""
return list(OPERATION_LIST.values()) return list(OPERATION_LIST.values())
@property
def current_fan_mode(self):
"""Return the fan setting."""
if self.ac_mode:
return FAN_MODES_LIST.get(self._current_fan)
else:
return None
@property
def fan_list(self):
"""List of available fan modes."""
if self.ac_mode:
return list(FAN_MODES_LIST.values())
else:
return None
@property @property
def temperature_unit(self): def temperature_unit(self):
"""Return the unit of measurement used by the platform.""" """Return the unit of measurement used by the platform."""
@ -205,27 +243,27 @@ class TadoClimate(ClimateDevice):
if 'sensorDataPoints' in data: if 'sensorDataPoints' in data:
sensor_data = data['sensorDataPoints'] sensor_data = data['sensorDataPoints']
temperature = float(
sensor_data['insideTemperature']['celsius']) unit = TEMP_CELSIUS
humidity = float(
sensor_data['humidity']['percentage']) if 'insideTemperature' in sensor_data:
setting = 0 temperature = float(
sensor_data['insideTemperature']['celsius'])
self._cur_temp = self.hass.config.units.temperature(
temperature, unit)
if 'humidity' in sensor_data:
humidity = float(
sensor_data['humidity']['percentage'])
self._cur_humidity = humidity
# temperature setting will not exist when device is off # temperature setting will not exist when device is off
if 'temperature' in data['setting'] and \ if 'temperature' in data['setting'] and \
data['setting']['temperature'] is not None: data['setting']['temperature'] is not None:
setting = float( setting = float(
data['setting']['temperature']['celsius']) data['setting']['temperature']['celsius'])
self._target_temp = self.hass.config.units.temperature(
unit = TEMP_CELSIUS setting, unit)
self._cur_temp = self.hass.config.units.temperature(
temperature, unit)
self._target_temp = self.hass.config.units.temperature(
setting, unit)
self._cur_humidity = humidity
if 'tadoMode' in data: if 'tadoMode' in data:
mode = data['tadoMode'] mode = data['tadoMode']
@ -235,29 +273,39 @@ class TadoClimate(ClimateDevice):
power = data['setting']['power'] power = data['setting']['power']
if power == 'OFF': if power == 'OFF':
self._current_operation = CONST_MODE_OFF self._current_operation = CONST_MODE_OFF
self._current_fan = CONST_MODE_OFF
# There is no overlay, the mode will always be
# "SMART_SCHEDULE"
self._overlay_mode = CONST_MODE_SMART_SCHEDULE
self._device_is_active = False self._device_is_active = False
else: else:
self._device_is_active = True self._device_is_active = True
if 'overlay' in data and data['overlay'] is not None: if self._device_is_active:
overlay = True
termination = data['overlay']['termination']['type']
else:
overlay = False overlay = False
termination = "" overlay_data = None
termination = self._current_operation
cooling = False
fan_speed = CONST_MODE_OFF
# If you set mode manualy to off, there will be an overlay if 'overlay' in data:
# and a termination, but we want to see the mode "OFF" overlay_data = data['overlay']
overlay = overlay_data is not None
if overlay:
termination = overlay_data['termination']['type']
if 'setting' in overlay_data:
cooling = overlay_data['setting']['mode'] == 'COOL'
fan_speed = overlay_data['setting']['fanSpeed']
# If you set mode manualy to off, there will be an overlay
# and a termination, but we want to see the mode "OFF"
if overlay and self._device_is_active:
# There is an overlay the device is on
self._overlay_mode = termination self._overlay_mode = termination
self._current_operation = termination self._current_operation = termination
else: self._cooling = cooling
# There is no overlay, the mode will always be self._current_fan = fan_speed
# "SMART_SCHEDULE"
self._overlay_mode = CONST_MODE_SMART_SCHEDULE
self._current_operation = CONST_MODE_SMART_SCHEDULE
def _control_heating(self): def _control_heating(self):
"""Send new target temperature to mytado.""" """Send new target temperature to mytado."""

View File

@ -0,0 +1,134 @@
"""
Support to control a Zehnder ComfoAir Q350/450/600 ventilation unit.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/comfoconnect/
"""
import logging
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.const import (
CONF_HOST, CONF_NAME, CONF_TOKEN, CONF_PIN, EVENT_HOMEASSISTANT_STOP)
from homeassistant.helpers import (discovery)
from homeassistant.helpers.dispatcher import (dispatcher_send)
REQUIREMENTS = ['pycomfoconnect==0.3']
_LOGGER = logging.getLogger(__name__)
DOMAIN = 'comfoconnect'
SIGNAL_COMFOCONNECT_UPDATE_RECEIVED = 'comfoconnect_update_received'
ATTR_CURRENT_TEMPERATURE = 'current_temperature'
ATTR_CURRENT_HUMIDITY = 'current_humidity'
ATTR_OUTSIDE_TEMPERATURE = 'outside_temperature'
ATTR_OUTSIDE_HUMIDITY = 'outside_humidity'
ATTR_AIR_FLOW_SUPPLY = 'air_flow_supply'
ATTR_AIR_FLOW_EXHAUST = 'air_flow_exhaust'
CONF_USER_AGENT = 'user_agent'
DEFAULT_NAME = 'ComfoAirQ'
DEFAULT_PIN = 0
DEFAULT_TOKEN = '00000000000000000000000000000001'
DEFAULT_USER_AGENT = 'Home Assistant'
DEVICE = None
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_TOKEN, default=DEFAULT_TOKEN):
vol.Length(min=32, max=32, msg='invalid token'),
vol.Optional(CONF_USER_AGENT, default=DEFAULT_USER_AGENT): cv.string,
vol.Optional(CONF_PIN, default=DEFAULT_PIN): cv.positive_int,
}),
}, extra=vol.ALLOW_EXTRA)
def setup(hass, config):
"""Set up the ComfoConnect bridge."""
from pycomfoconnect import (Bridge)
conf = config[DOMAIN]
host = conf.get(CONF_HOST)
name = conf.get(CONF_NAME)
token = conf.get(CONF_TOKEN)
user_agent = conf.get(CONF_USER_AGENT)
pin = conf.get(CONF_PIN)
# Run discovery on the configured ip
bridges = Bridge.discover(host)
if not bridges:
_LOGGER.error("Could not connect to ComfoConnect bridge on %s", host)
return False
bridge = bridges[0]
_LOGGER.info("Bridge found: %s (%s)", bridge.uuid.hex(), bridge.host)
# Setup ComfoConnect Bridge
ccb = ComfoConnectBridge(hass, bridge, name, token, user_agent, pin)
hass.data[DOMAIN] = ccb
# Start connection with bridge
ccb.connect()
# Schedule disconnect on shutdown
def _shutdown(_event):
ccb.disconnect()
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, _shutdown)
# Load platforms
discovery.load_platform(hass, 'fan', DOMAIN, {}, config)
return True
class ComfoConnectBridge(object):
"""Representation of a ComfoConnect bridge."""
def __init__(self, hass, bridge, name, token, friendly_name, pin):
"""Initialize the ComfoConnect bridge."""
from pycomfoconnect import (ComfoConnect)
self.data = {}
self.name = name
self.hass = hass
self.comfoconnect = ComfoConnect(
bridge=bridge, local_uuid=bytes.fromhex(token),
local_devicename=friendly_name, pin=pin)
self.comfoconnect.callback_sensor = self.sensor_callback
def connect(self):
"""Connect with the bridge."""
_LOGGER.debug("Connecting with bridge")
self.comfoconnect.connect(True)
def disconnect(self):
"""Disconnect from the bridge."""
_LOGGER.debug("Disconnecting from bridge")
self.comfoconnect.disconnect()
def sensor_callback(self, var, value):
"""Callback function for sensor updates."""
_LOGGER.debug("Got value from bridge: %d = %d", var, value)
from pycomfoconnect import (
SENSOR_TEMPERATURE_EXTRACT, SENSOR_TEMPERATURE_OUTDOOR)
if var in [SENSOR_TEMPERATURE_EXTRACT, SENSOR_TEMPERATURE_OUTDOOR]:
self.data[var] = value / 10
else:
self.data[var] = value
# Notify listeners that we have received an update
dispatcher_send(self.hass, SIGNAL_COMFOCONNECT_UPDATE_RECEIVED, var)
def subscribe_sensor(self, sensor_id):
"""Subscribe for the specified sensor."""
self.comfoconnect.register_sensor(sensor_id)

View File

@ -40,6 +40,8 @@ DEVICE_CLASSES = [
'garage', # Garage door control 'garage', # Garage door control
] ]
DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.In(DEVICE_CLASSES))
SUPPORT_OPEN = 1 SUPPORT_OPEN = 1
SUPPORT_CLOSE = 2 SUPPORT_CLOSE = 2
SUPPORT_SET_POSITION = 4 SUPPORT_SET_POSITION = 4

View File

@ -0,0 +1,185 @@
"""
Support for KNX covers.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/cover.knx/
"""
import logging
import voluptuous as vol
from homeassistant.components.cover import (
CoverDevice, PLATFORM_SCHEMA, ATTR_POSITION, DEVICE_CLASSES_SCHEMA,
SUPPORT_OPEN, SUPPORT_CLOSE, SUPPORT_SET_POSITION, SUPPORT_STOP,
SUPPORT_SET_TILT_POSITION
)
from homeassistant.components.knx import (KNXConfig, KNXMultiAddressDevice)
from homeassistant.const import (CONF_NAME, CONF_DEVICE_CLASS)
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
CONF_GETPOSITION_ADDRESS = 'getposition_address'
CONF_SETPOSITION_ADDRESS = 'setposition_address'
CONF_GETANGLE_ADDRESS = 'getangle_address'
CONF_SETANGLE_ADDRESS = 'setangle_address'
CONF_STOP = 'stop_address'
CONF_UPDOWN = 'updown_address'
CONF_INVERT_POSITION = 'invert_position'
CONF_INVERT_ANGLE = 'invert_angle'
DEFAULT_NAME = 'KNX Cover'
DEPENDENCIES = ['knx']
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_UPDOWN): cv.string,
vol.Required(CONF_STOP): cv.string,
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_GETPOSITION_ADDRESS): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_SETPOSITION_ADDRESS): cv.string,
vol.Optional(CONF_INVERT_POSITION, default=False): cv.boolean,
vol.Inclusive(CONF_GETANGLE_ADDRESS, 'angle'): cv.string,
vol.Inclusive(CONF_SETANGLE_ADDRESS, 'angle'): cv.string,
vol.Optional(CONF_INVERT_ANGLE, default=False): cv.boolean,
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Create and add an entity based on the configuration."""
add_devices([KNXCover(hass, KNXConfig(config))])
class KNXCover(KNXMultiAddressDevice, CoverDevice):
"""Representation of a KNX cover. e.g. a rollershutter."""
def __init__(self, hass, config):
"""Initialize the cover."""
KNXMultiAddressDevice.__init__(
self, hass, config,
['updown', 'stop'], # required
optional=['setposition', 'getposition',
'getangle', 'setangle']
)
self._device_class = config.config.get(CONF_DEVICE_CLASS)
self._invert_position = config.config.get(CONF_INVERT_POSITION)
self._invert_angle = config.config.get(CONF_INVERT_ANGLE)
self._hass = hass
self._current_pos = None
self._target_pos = None
self._current_tilt = None
self._target_tilt = None
self._supported_features = SUPPORT_OPEN | SUPPORT_CLOSE | \
SUPPORT_SET_POSITION | SUPPORT_STOP
# Tilt is only supported, if there is a angle get and set address
if CONF_SETANGLE_ADDRESS in config.config:
_LOGGER.debug("%s: Tilt supported at addresses %s, %s",
self.name, config.config.get(CONF_SETANGLE_ADDRESS),
config.config.get(CONF_GETANGLE_ADDRESS))
self._supported_features = self._supported_features | \
SUPPORT_SET_TILT_POSITION
@property
def should_poll(self):
"""Polling is needed for the KNX cover."""
return True
@property
def supported_features(self):
"""Flag supported features."""
return self._supported_features
@property
def is_closed(self):
"""Return if the cover is closed."""
if self.current_cover_position is not None:
if self.current_cover_position > 0:
return False
else:
return True
@property
def current_cover_position(self):
"""Return current position of cover.
None is unknown, 0 is closed, 100 is fully open.
"""
return self._current_pos
@property
def target_position(self):
"""Return the position we are trying to reach: 0 - 100."""
return self._target_pos
@property
def current_cover_tilt_position(self):
"""Return current position of cover.
None is unknown, 0 is closed, 100 is fully open.
"""
return self._current_tilt
@property
def target_tilt(self):
"""Return the tilt angle (in %) we are trying to reach: 0 - 100."""
return self._target_tilt
def set_cover_position(self, **kwargs):
"""Set new target position."""
position = kwargs.get(ATTR_POSITION)
if position is None:
return
if self._invert_position:
position = 100-position
self._target_pos = position
self.set_percentage('setposition', position)
_LOGGER.debug("%s: Set target position to %d", self.name, position)
def update(self):
"""Update device state."""
super().update()
value = self.get_percentage('getposition')
if value is not None:
self._current_pos = value
if self._invert_position:
self._current_pos = 100-value
_LOGGER.debug("%s: position = %d", self.name, value)
if self._supported_features & SUPPORT_SET_TILT_POSITION:
value = self.get_percentage('getangle')
if value is not None:
self._current_tilt = value
if self._invert_angle:
self._current_tilt = 100-value
_LOGGER.debug("%s: tilt = %d", self.name, value)
def open_cover(self, **kwargs):
"""Open the cover."""
_LOGGER.debug("%s: open: updown = 0", self.name)
self.set_int_value('updown', 0)
def close_cover(self, **kwargs):
"""Close the cover."""
_LOGGER.debug("%s: open: updown = 1", self.name)
self.set_int_value('updown', 1)
def stop_cover(self, **kwargs):
"""Stop the cover movement."""
_LOGGER.debug("%s: stop: stop = 1", self.name)
self.set_int_value('stop', 1)
def set_cover_tilt_position(self, tilt_position, **kwargs):
"""Move the cover til to a specific position."""
if self._invert_angle:
tilt_position = 100-tilt_position
self._target_tilt = round(tilt_position, -1)
self.set_percentage('setangle', tilt_position)
@property
def device_class(self):
"""Return the class of this device, from component DEVICE_CLASSES."""
return self._device_class

View File

@ -0,0 +1,354 @@
"""
Support for covers which integrate with other components.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/cover.template/
"""
import asyncio
import logging
import voluptuous as vol
from homeassistant.core import callback
from homeassistant.components.cover import (
ENTITY_ID_FORMAT, CoverDevice, PLATFORM_SCHEMA,
SUPPORT_OPEN_TILT, SUPPORT_CLOSE_TILT, SUPPORT_STOP_TILT,
SUPPORT_SET_TILT_POSITION, SUPPORT_OPEN, SUPPORT_CLOSE, SUPPORT_STOP,
SUPPORT_SET_POSITION, ATTR_POSITION, ATTR_TILT_POSITION)
from homeassistant.const import (
CONF_FRIENDLY_NAME, CONF_ENTITY_ID,
EVENT_HOMEASSISTANT_START, MATCH_ALL,
CONF_VALUE_TEMPLATE, CONF_ICON_TEMPLATE,
STATE_OPEN, STATE_CLOSED)
from homeassistant.exceptions import TemplateError
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import async_generate_entity_id
from homeassistant.helpers.event import async_track_state_change
from homeassistant.helpers.restore_state import async_get_last_state
from homeassistant.helpers.script import Script
_LOGGER = logging.getLogger(__name__)
_VALID_STATES = [STATE_OPEN, STATE_CLOSED, 'true', 'false']
CONF_COVERS = 'covers'
CONF_POSITION_TEMPLATE = 'position_template'
CONF_TILT_TEMPLATE = 'tilt_template'
OPEN_ACTION = 'open_cover'
CLOSE_ACTION = 'close_cover'
STOP_ACTION = 'stop_cover'
POSITION_ACTION = 'set_cover_position'
TILT_ACTION = 'set_cover_tilt_position'
CONF_VALUE_OR_POSITION_TEMPLATE = 'value_or_position'
TILT_FEATURES = (SUPPORT_OPEN_TILT | SUPPORT_CLOSE_TILT | SUPPORT_STOP_TILT |
SUPPORT_SET_TILT_POSITION)
COVER_SCHEMA = vol.Schema({
vol.Required(OPEN_ACTION): cv.SCRIPT_SCHEMA,
vol.Required(CLOSE_ACTION): cv.SCRIPT_SCHEMA,
vol.Required(STOP_ACTION): cv.SCRIPT_SCHEMA,
vol.Exclusive(CONF_POSITION_TEMPLATE,
CONF_VALUE_OR_POSITION_TEMPLATE): cv.template,
vol.Exclusive(CONF_VALUE_TEMPLATE,
CONF_VALUE_OR_POSITION_TEMPLATE): cv.template,
vol.Optional(CONF_POSITION_TEMPLATE): cv.template,
vol.Optional(CONF_TILT_TEMPLATE): cv.template,
vol.Optional(CONF_ICON_TEMPLATE): cv.template,
vol.Optional(POSITION_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(TILT_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_FRIENDLY_NAME, default=None): cv.string,
vol.Optional(CONF_ENTITY_ID): cv.entity_ids
})
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_COVERS): vol.Schema({cv.slug: COVER_SCHEMA}),
})
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up the Template cover."""
covers = []
for device, device_config in config[CONF_COVERS].items():
friendly_name = device_config.get(CONF_FRIENDLY_NAME, device)
state_template = device_config.get(CONF_VALUE_TEMPLATE)
position_template = device_config.get(CONF_POSITION_TEMPLATE)
tilt_template = device_config.get(CONF_TILT_TEMPLATE)
icon_template = device_config.get(CONF_ICON_TEMPLATE)
open_action = device_config[OPEN_ACTION]
close_action = device_config[CLOSE_ACTION]
stop_action = device_config[STOP_ACTION]
position_action = device_config.get(POSITION_ACTION)
tilt_action = device_config.get(TILT_ACTION)
if position_template is None and state_template is None:
_LOGGER.error('Must specify either %s' or '%s',
CONF_VALUE_TEMPLATE, CONF_VALUE_TEMPLATE)
continue
template_entity_ids = set()
if state_template is not None:
temp_ids = state_template.extract_entities()
if str(temp_ids) != MATCH_ALL:
template_entity_ids |= set(temp_ids)
if position_template is not None:
temp_ids = position_template.extract_entities()
if str(temp_ids) != MATCH_ALL:
template_entity_ids |= set(temp_ids)
if tilt_template is not None:
temp_ids = tilt_template.extract_entities()
if str(temp_ids) != MATCH_ALL:
template_entity_ids |= set(temp_ids)
if icon_template is not None:
temp_ids = icon_template.extract_entities()
if str(temp_ids) != MATCH_ALL:
template_entity_ids |= set(temp_ids)
if not template_entity_ids:
template_entity_ids = MATCH_ALL
entity_ids = device_config.get(CONF_ENTITY_ID, template_entity_ids)
covers.append(
CoverTemplate(
hass,
device, friendly_name, state_template,
position_template, tilt_template, icon_template,
open_action, close_action, stop_action,
position_action, tilt_action, entity_ids
)
)
if not covers:
_LOGGER.error("No covers added")
return False
async_add_devices(covers, True)
return True
class CoverTemplate(CoverDevice):
"""Representation of a Template cover."""
def __init__(self, hass, device_id, friendly_name, state_template,
position_template, tilt_template, icon_template,
open_action, close_action, stop_action,
position_action, tilt_action, entity_ids):
"""Initialize the Template cover."""
self.hass = hass
self.entity_id = async_generate_entity_id(
ENTITY_ID_FORMAT, device_id, hass=hass)
self._name = friendly_name
self._template = state_template
self._position_template = position_template
self._tilt_template = tilt_template
self._icon_template = icon_template
self._open_script = Script(hass, open_action)
self._close_script = Script(hass, close_action)
self._stop_script = Script(hass, stop_action)
self._position_script = None
if position_action is not None:
self._position_script = Script(hass, position_action)
self._tilt_script = None
if tilt_action is not None:
self._tilt_script = Script(hass, tilt_action)
self._icon = None
self._position = None
self._tilt_value = None
self._entities = entity_ids
if self._template is not None:
self._template.hass = self.hass
if self._position_template is not None:
self._position_template.hass = self.hass
if self._tilt_template is not None:
self._tilt_template.hass = self.hass
if self._icon_template is not None:
self._icon_template.hass = self.hass
@asyncio.coroutine
def async_added_to_hass(self):
"""Register callbacks."""
state = yield from async_get_last_state(self.hass, self.entity_id)
if state:
self._position = 100 if state.state == STATE_OPEN else 0
@callback
def template_cover_state_listener(entity, old_state, new_state):
"""Handle target device state changes."""
self.hass.async_add_job(self.async_update_ha_state(True))
@callback
def template_cover_startup(event):
"""Update template on startup."""
async_track_state_change(
self.hass, self._entities, template_cover_state_listener)
self.hass.async_add_job(self.async_update_ha_state(True))
self.hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_START, template_cover_startup)
@property
def name(self):
"""Return the name of the cover."""
return self._name
@property
def is_closed(self):
"""Return if the cover is closed."""
return self._position == 0
@property
def current_cover_position(self):
"""Return current position of cover.
None is unknown, 0 is closed, 100 is fully open.
"""
return self._position
@property
def current_cover_tilt_position(self):
"""Return current position of cover tilt.
None is unknown, 0 is closed, 100 is fully open.
"""
return self._tilt_value
@property
def icon(self):
"""Return the icon to use in the frontend, if any."""
return self._icon
@property
def supported_features(self):
"""Flag supported features."""
supported_features = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_STOP
if self.current_cover_position is not None:
supported_features |= SUPPORT_SET_POSITION
if self.current_cover_tilt_position is not None:
supported_features |= TILT_FEATURES
return supported_features
@property
def should_poll(self):
"""Return the polling state."""
return False
@asyncio.coroutine
def async_open_cover(self, **kwargs):
"""Move the cover up."""
self.hass.async_add_job(self._open_script.async_run())
@asyncio.coroutine
def async_close_cover(self, **kwargs):
"""Move the cover down."""
self.hass.async_add_job(self._close_script.async_run())
@asyncio.coroutine
def async_stop_cover(self, **kwargs):
"""Fire the stop action."""
self.hass.async_add_job(self._stop_script.async_run())
@asyncio.coroutine
def async_set_cover_position(self, **kwargs):
"""Set cover position."""
if ATTR_POSITION not in kwargs:
return
self._position = kwargs[ATTR_POSITION]
self.hass.async_add_job(self._position_script.async_run(
{"position": self._position}))
@asyncio.coroutine
def async_open_cover_tilt(self, **kwargs):
"""Tilt the cover open."""
self._tilt_value = 100
self.hass.async_add_job(self._tilt_script.async_run(
{"tilt": self._tilt_value}))
@asyncio.coroutine
def async_close_cover_tilt(self, **kwargs):
"""Tilt the cover closed."""
self._tilt_value = 0
self.hass.async_add_job(self._tilt_script.async_run(
{"tilt": self._tilt_value}))
@asyncio.coroutine
def async_set_cover_tilt_position(self, **kwargs):
"""Move the cover tilt to a specific position."""
if ATTR_TILT_POSITION not in kwargs:
return
self._tilt_value = kwargs[ATTR_TILT_POSITION]
self.hass.async_add_job(self._tilt_script.async_run(
{"tilt": self._tilt_value}))
@asyncio.coroutine
def async_update(self):
"""Update the state from the template."""
if self._template is not None:
try:
state = self._template.async_render().lower()
if state in _VALID_STATES:
if state in ('true', STATE_OPEN):
self._position = 100
else:
self._position = 0
else:
_LOGGER.error(
'Received invalid cover is_on state: %s. Expected: %s',
state, ', '.join(_VALID_STATES))
self._position = None
except TemplateError as ex:
_LOGGER.error(ex)
self._position = None
if self._position_template is not None:
try:
state = float(self._position_template.async_render())
if state < 0 or state > 100:
self._position = None
_LOGGER.error("Cover position value must be"
" between 0 and 100."
" Value was: %.2f", state)
else:
self._position = state
except TemplateError as ex:
_LOGGER.error(ex)
self._position = None
except ValueError as ex:
_LOGGER.error(ex)
self._position = None
if self._tilt_template is not None:
try:
state = float(self._tilt_template.async_render())
if state < 0 or state > 100:
self._tilt_value = None
_LOGGER.error("Tilt value must be between 0 and 100."
" Value was: %.2f", state)
else:
self._tilt_value = state
except TemplateError as ex:
_LOGGER.error(ex)
self._tilt_value = None
except ValueError as ex:
_LOGGER.error(ex)
self._tilt_value = None
if self._icon_template is not None:
try:
self._icon = self._icon_template.async_render()
except TemplateError as ex:
if ex.args and ex.args[0].startswith(
"UndefinedError: 'None' has no attribute"):
# Common during HA startup - so just a warning
_LOGGER.warning('Could not render icon template %s,'
' the state is unknown.', self._name)
return
self._icon = super().icon
_LOGGER.error('Could not render icon template %s: %s',
self._name, ex)

View File

@ -210,6 +210,7 @@ def async_setup(hass, config):
description=("Press the button on the bridge to register Philips " description=("Press the button on the bridge to register Philips "
"Hue with Home Assistant."), "Hue with Home Assistant."),
description_image="/static/images/config_philips_hue.jpg", description_image="/static/images/config_philips_hue.jpg",
fields=[{'id': 'username', 'name': 'Username'}],
submit_caption="I have pressed the button" submit_caption="I have pressed the button"
) )
configurator_ids.append(request_id) configurator_ids.append(request_id)

View File

@ -0,0 +1,110 @@
"""Support for Linksys Smart Wifi routers."""
import logging
import threading
from datetime import timedelta
import requests
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.device_tracker import (
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
from homeassistant.const import CONF_HOST
from homeassistant.util import Throttle
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
DEFAULT_TIMEOUT = 10
_LOGGER = logging.getLogger(__name__)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,
})
def get_scanner(hass, config):
"""Validate the configuration and return a Linksys AP scanner."""
try:
return LinksysSmartWifiDeviceScanner(config[DOMAIN])
except ConnectionError:
return None
class LinksysSmartWifiDeviceScanner(DeviceScanner):
"""This class queries a Linksys Access Point."""
def __init__(self, config):
"""Initialize the scanner."""
self.host = config[CONF_HOST]
self.lock = threading.Lock()
self.last_results = {}
# Check if the access point is accessible
response = self._make_request()
if not response.status_code == 200:
raise ConnectionError("Cannot connect to Linksys Access Point")
def scan_devices(self):
"""Scan for new devices and return a list with device IDs (MACs)."""
self._update_info()
return self.last_results.keys()
def get_device_name(self, mac):
"""Return the name (if known) of the device."""
return self.last_results.get(mac)
@Throttle(MIN_TIME_BETWEEN_SCANS)
def _update_info(self):
"""Check for connected devices."""
with self.lock:
_LOGGER.info("Checking Linksys Smart Wifi")
self.last_results = {}
response = self._make_request()
if response.status_code != 200:
_LOGGER.error(
"Got HTTP status code %d when getting device list",
response.status_code)
return False
try:
data = response.json()
result = data["responses"][0]
devices = result["output"]["devices"]
for device in devices:
macs = device["knownMACAddresses"]
if not macs:
_LOGGER.warning(
"Skipping device without known MAC address")
continue
mac = macs[-1]
connections = device["connections"]
if not connections:
_LOGGER.debug("Device %s is not connected", mac)
continue
name = device["friendlyName"]
properties = device["properties"]
for prop in properties:
if prop["name"] == "userDeviceName":
name = prop["value"]
_LOGGER.debug("Device %s is connected", mac)
self.last_results[mac] = name
except (KeyError, IndexError):
_LOGGER.exception("Router returned unexpected response")
return False
return True
def _make_request(self):
# Weirdly enough, this doesn't seem to require authentication
data = [{
"request": {
"sinceRevision": 0
},
"action": "http://linksys.com/jnap/devicelist/GetDevices"
}]
headers = {"X-JNAP-Action": "http://linksys.com/jnap/core/Transaction"}
return requests.post('http://{}/JNAP/'.format(self.host),
timeout=DEFAULT_TIMEOUT,
headers=headers,
json=data)

View File

@ -158,6 +158,11 @@ class MikrotikScanner(DeviceScanner):
for device in devices for device in devices
} }
else: else:
self.last_results = mac_names self.last_results = {
device.get('mac-address'):
mac_names.get(device.get('mac-address'))
for device in device_names
if device.get('active-address')
}
return True return True

View File

@ -21,7 +21,7 @@ from homeassistant.components import zone as zone_comp
from homeassistant.components.device_tracker import PLATFORM_SCHEMA from homeassistant.components.device_tracker import PLATFORM_SCHEMA
DEPENDENCIES = ['mqtt'] DEPENDENCIES = ['mqtt']
REQUIREMENTS = ['libnacl==1.5.0'] REQUIREMENTS = ['libnacl==1.5.1']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

32
homeassistant/components/device_tracker/ubus.py Executable file → Normal file
View File

@ -18,6 +18,7 @@ from homeassistant.components.device_tracker import (
DOMAIN, PLATFORM_SCHEMA, DeviceScanner) DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
from homeassistant.util import Throttle from homeassistant.util import Throttle
from homeassistant.exceptions import HomeAssistantError
# Return cached results if last scan was less then this time ago. # Return cached results if last scan was less then this time ago.
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5) MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
@ -38,6 +39,23 @@ def get_scanner(hass, config):
return scanner if scanner.success_init else None return scanner if scanner.success_init else None
def _refresh_on_acccess_denied(func):
"""If remove rebooted, it lost our session so rebuld one and try again."""
def decorator(self, *args, **kwargs):
"""Wrapper function to refresh session_id on PermissionError."""
try:
return func(self, *args, **kwargs)
except PermissionError:
_LOGGER.warning("Invalid session detected." +
" Tryign to refresh session_id and re-run the rpc")
self.session_id = _get_session_id(self.url, self.username,
self.password)
return func(self, *args, **kwargs)
return decorator
class UbusDeviceScanner(DeviceScanner): class UbusDeviceScanner(DeviceScanner):
""" """
This class queries a wireless router running OpenWrt firmware. This class queries a wireless router running OpenWrt firmware.
@ -48,14 +66,16 @@ class UbusDeviceScanner(DeviceScanner):
def __init__(self, config): def __init__(self, config):
"""Initialize the scanner.""" """Initialize the scanner."""
host = config[CONF_HOST] host = config[CONF_HOST]
username, password = config[CONF_USERNAME], config[CONF_PASSWORD] self.username = config[CONF_USERNAME]
self.password = config[CONF_PASSWORD]
self.parse_api_pattern = re.compile(r"(?P<param>\w*) = (?P<value>.*);") self.parse_api_pattern = re.compile(r"(?P<param>\w*) = (?P<value>.*);")
self.lock = threading.Lock() self.lock = threading.Lock()
self.last_results = {} self.last_results = {}
self.url = 'http://{}/ubus'.format(host) self.url = 'http://{}/ubus'.format(host)
self.session_id = _get_session_id(self.url, username, password) self.session_id = _get_session_id(self.url, self.username,
self.password)
self.hostapd = [] self.hostapd = []
self.leasefile = None self.leasefile = None
self.mac2name = None self.mac2name = None
@ -66,6 +86,7 @@ class UbusDeviceScanner(DeviceScanner):
self._update_info() self._update_info()
return self.last_results return self.last_results
@_refresh_on_acccess_denied
def get_device_name(self, device): def get_device_name(self, device):
"""Return the name of the given device or None if we don't know.""" """Return the name of the given device or None if we don't know."""
with self.lock: with self.lock:
@ -95,6 +116,7 @@ class UbusDeviceScanner(DeviceScanner):
return self.mac2name.get(device.upper(), None) return self.mac2name.get(device.upper(), None)
@Throttle(MIN_TIME_BETWEEN_SCANS) @Throttle(MIN_TIME_BETWEEN_SCANS)
@_refresh_on_acccess_denied
def _update_info(self): def _update_info(self):
"""Ensure the information from the Luci router is up to date. """Ensure the information from the Luci router is up to date.
@ -142,6 +164,12 @@ def _req_json_rpc(url, session_id, rpcmethod, subsystem, method, **params):
if res.status_code == 200: if res.status_code == 200:
response = res.json() response = res.json()
if 'error' in response:
if 'message' in response['error'] and \
response['error']['message'] == "Access denied":
raise PermissionError(response['error']['message'])
else:
raise HomeAssistantError(response['error']['message'])
if rpcmethod == "call": if rpcmethod == "call":
try: try:

View File

@ -13,7 +13,7 @@ from homeassistant.const import CONF_ACCESS_TOKEN
from homeassistant.util import Throttle from homeassistant.util import Throttle
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['python-digitalocean==1.11'] REQUIREMENTS = ['python-digitalocean==1.12']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -29,7 +29,7 @@ ATTR_VCPUS = 'vcpus'
CONF_DROPLETS = 'droplets' CONF_DROPLETS = 'droplets'
DIGITAL_OCEAN = None DATA_DIGITAL_OCEAN = 'data_do'
DIGITAL_OCEAN_PLATFORMS = ['switch', 'binary_sensor'] DIGITAL_OCEAN_PLATFORMS = ['switch', 'binary_sensor']
DOMAIN = 'digital_ocean' DOMAIN = 'digital_ocean'
@ -47,13 +47,14 @@ def setup(hass, config):
conf = config[DOMAIN] conf = config[DOMAIN]
access_token = conf.get(CONF_ACCESS_TOKEN) access_token = conf.get(CONF_ACCESS_TOKEN)
global DIGITAL_OCEAN digital = DigitalOcean(access_token)
DIGITAL_OCEAN = DigitalOcean(access_token)
if not DIGITAL_OCEAN.manager.get_account(): if not digital.manager.get_account():
_LOGGER.error("No Digital Ocean account found for the given API Token") _LOGGER.error("No Digital Ocean account found for the given API Token")
return False return False
hass.data[DATA_DIGITAL_OCEAN] = digital
return True return True

View File

@ -55,6 +55,7 @@ SERVICE_HANDLERS = {
'apple_tv': ('media_player', 'apple_tv'), 'apple_tv': ('media_player', 'apple_tv'),
'frontier_silicon': ('media_player', 'frontier_silicon'), 'frontier_silicon': ('media_player', 'frontier_silicon'),
'openhome': ('media_player', 'openhome'), 'openhome': ('media_player', 'openhome'),
'harmony': ('remote', 'harmony'),
'bose_soundtouch': ('media_player', 'soundtouch'), 'bose_soundtouch': ('media_player', 'soundtouch'),
} }

View File

@ -0,0 +1,118 @@
"""
Platform to control a Zehnder ComfoAir Q350/450/600 ventilation unit.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/fan.comfoconnect/
"""
import logging
from homeassistant.components.comfoconnect import (
DOMAIN, ComfoConnectBridge, SIGNAL_COMFOCONNECT_UPDATE_RECEIVED)
from homeassistant.components.fan import (
FanEntity, SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH,
SUPPORT_SET_SPEED)
from homeassistant.const import STATE_UNKNOWN
from homeassistant.helpers.dispatcher import (dispatcher_connect)
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['comfoconnect']
SPEED_MAPPING = {
0: SPEED_OFF,
1: SPEED_LOW,
2: SPEED_MEDIUM,
3: SPEED_HIGH
}
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the ComfoConnect fan platform."""
ccb = hass.data[DOMAIN]
add_devices([ComfoConnectFan(hass, name=ccb.name, ccb=ccb)], True)
return
class ComfoConnectFan(FanEntity):
"""Representation of the ComfoConnect fan platform."""
def __init__(self, hass, name, ccb: ComfoConnectBridge):
"""Initialize the ComfoConnect fan."""
from pycomfoconnect import SENSOR_FAN_SPEED_MODE
self._ccb = ccb
self._name = name
# Ask the bridge to keep us updated
self._ccb.comfoconnect.register_sensor(SENSOR_FAN_SPEED_MODE)
def _handle_update(var):
if var == SENSOR_FAN_SPEED_MODE:
_LOGGER.debug("Dispatcher update for %s", var)
self.schedule_update_ha_state()
# Register for dispatcher updates
dispatcher_connect(
hass, SIGNAL_COMFOCONNECT_UPDATE_RECEIVED, _handle_update)
@property
def name(self):
"""Return the name of the fan."""
return self._name
@property
def icon(self):
"""Return the icon to use in the frontend."""
return 'mdi:air-conditioner'
@property
def supported_features(self) -> int:
"""Flag supported features."""
return SUPPORT_SET_SPEED
@property
def speed(self):
"""Return the current fan mode."""
from pycomfoconnect import (SENSOR_FAN_SPEED_MODE)
try:
speed = self._ccb.data[SENSOR_FAN_SPEED_MODE]
return SPEED_MAPPING[speed]
except KeyError:
return STATE_UNKNOWN
@property
def speed_list(self):
"""List of available fan modes."""
return [SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH]
def turn_on(self, speed: str=None, **kwargs) -> None:
"""Turn on the fan."""
if speed is None:
speed = SPEED_LOW
self.set_speed(speed)
def turn_off(self) -> None:
"""Turn off the fan (to away)."""
self.set_speed(SPEED_OFF)
def set_speed(self, mode):
"""Set fan speed."""
_LOGGER.debug('Changing fan mode to %s.', mode)
from pycomfoconnect import (
CMD_FAN_MODE_AWAY, CMD_FAN_MODE_LOW, CMD_FAN_MODE_MEDIUM,
CMD_FAN_MODE_HIGH)
if mode == SPEED_OFF:
self._ccb.comfoconnect.cmd_rmi_request(CMD_FAN_MODE_AWAY)
elif mode == SPEED_LOW:
self._ccb.comfoconnect.cmd_rmi_request(CMD_FAN_MODE_LOW)
elif mode == SPEED_MEDIUM:
self._ccb.comfoconnect.cmd_rmi_request(CMD_FAN_MODE_MEDIUM)
elif mode == SPEED_HIGH:
self._ccb.comfoconnect.cmd_rmi_request(CMD_FAN_MODE_HIGH)
# Update current mode
self.schedule_update_ha_state()

View File

@ -0,0 +1,195 @@
"""
Support for Insteon fans via local hub control.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/fan.insteon_local/
"""
import json
import logging
import os
from datetime import timedelta
from homeassistant.components.fan import (
ATTR_SPEED, SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH,
SUPPORT_SET_SPEED, FanEntity)
from homeassistant.helpers.entity import ToggleEntity
from homeassistant.loader import get_component
import homeassistant.util as util
_CONFIGURING = {}
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['insteon_local']
DOMAIN = 'fan'
INSTEON_LOCAL_FANS_CONF = 'insteon_local_fans.conf'
MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(milliseconds=100)
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
SUPPORT_INSTEON_LOCAL = SUPPORT_SET_SPEED
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Insteon local fan platform."""
insteonhub = hass.data['insteon_local']
conf_fans = config_from_file(hass.config.path(INSTEON_LOCAL_FANS_CONF))
if len(conf_fans):
for device_id in conf_fans:
setup_fan(device_id, conf_fans[device_id], insteonhub, hass,
add_devices)
else:
linked = insteonhub.get_linked()
for device_id in linked:
if (linked[device_id]['cat_type'] == 'dimmer' and
linked[device_id]['sku'] == '2475F' and
device_id not in conf_fans):
request_configuration(device_id,
insteonhub,
linked[device_id]['model_name'] + ' ' +
linked[device_id]['sku'],
hass, add_devices)
def request_configuration(device_id, insteonhub, model, hass,
add_devices_callback):
"""Request configuration steps from the user."""
configurator = get_component('configurator')
# We got an error if this method is called while we are configuring
if device_id in _CONFIGURING:
configurator.notify_errors(
_CONFIGURING[device_id], 'Failed to register, please try again.')
return
def insteon_fan_config_callback(data):
"""The actions to do when our configuration callback is called."""
setup_fan(device_id, data.get('name'), insteonhub, hass,
add_devices_callback)
_CONFIGURING[device_id] = configurator.request_config(
hass, 'Insteon ' + model + ' addr: ' + device_id,
insteon_fan_config_callback,
description=('Enter a name for ' + model + ' Fan addr: ' + device_id),
entity_picture='/static/images/config_insteon.png',
submit_caption='Confirm',
fields=[{'id': 'name', 'name': 'Name', 'type': ''}]
)
def setup_fan(device_id, name, insteonhub, hass, add_devices_callback):
"""Set up the fan."""
if device_id in _CONFIGURING:
request_id = _CONFIGURING.pop(device_id)
configurator = get_component('configurator')
configurator.request_done(request_id)
_LOGGER.info("Device configuration done!")
conf_fans = config_from_file(hass.config.path(INSTEON_LOCAL_FANS_CONF))
if device_id not in conf_fans:
conf_fans[device_id] = name
if not config_from_file(
hass.config.path(INSTEON_LOCAL_FANS_CONF),
conf_fans):
_LOGGER.error("Failed to save configuration file")
device = insteonhub.fan(device_id)
add_devices_callback([InsteonLocalFanDevice(device, name)])
def config_from_file(filename, config=None):
"""Small configuration file management function."""
if config:
# We're writing configuration
try:
with open(filename, 'w') as fdesc:
fdesc.write(json.dumps(config))
except IOError as error:
_LOGGER.error('Saving config file failed: %s', error)
return False
return True
else:
# We're reading config
if os.path.isfile(filename):
try:
with open(filename, 'r') as fdesc:
return json.loads(fdesc.read())
except IOError as error:
_LOGGER.error("Reading configuration file failed: %s", error)
# This won't work yet
return False
else:
return {}
class InsteonLocalFanDevice(FanEntity):
"""An abstract Class for an Insteon node."""
def __init__(self, node, name):
"""Initialize the device."""
self.node = node
self.node.deviceName = name
self._speed = SPEED_OFF
@property
def name(self):
"""Return the the name of the node."""
return self.node.deviceName
@property
def unique_id(self):
"""Return the ID of this Insteon node."""
return 'insteon_local_{}_fan'.format(self.node.device_id)
@property
def speed(self) -> str:
"""Return the current speed."""
return self._speed
@property
def speed_list(self: ToggleEntity) -> list:
"""Get the list of available speeds."""
return [SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH]
@util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS)
def update(self):
"""Update state of the fan."""
resp = self.node.status()
if 'cmd2' in resp:
if resp['cmd2'] == '00':
self._speed = SPEED_OFF
elif resp['cmd2'] == '55':
self._speed = SPEED_LOW
elif resp['cmd2'] == 'AA':
self._speed = SPEED_MEDIUM
elif resp['cmd2'] == 'FF':
self._speed = SPEED_HIGH
@property
def supported_features(self):
"""Flag supported features."""
return SUPPORT_INSTEON_LOCAL
def turn_on(self: ToggleEntity, speed: str=None, **kwargs) -> None:
"""Turn device on."""
if speed is None:
if ATTR_SPEED in kwargs:
speed = kwargs[ATTR_SPEED]
else:
speed = SPEED_MEDIUM
self.set_speed(speed)
def turn_off(self: ToggleEntity, **kwargs) -> None:
"""Turn device off."""
self.node.off()
def set_speed(self: ToggleEntity, speed: str) -> None:
"""Set the speed of the fan."""
if self.node.on(speed):
self._speed = speed

View File

@ -3,21 +3,21 @@
FINGERPRINTS = { FINGERPRINTS = {
"compatibility.js": "8e4c44b5f4288cc48ec1ba94a9bec812", "compatibility.js": "8e4c44b5f4288cc48ec1ba94a9bec812",
"core.js": "d4a7cb8c80c62b536764e0e81385f6aa", "core.js": "d4a7cb8c80c62b536764e0e81385f6aa",
"frontend.html": "cca45decbed803e7f0ec0b4f6e18fe53", "frontend.html": "f170a7221615ca2839cb8fd51a82f50a",
"mdi.html": "1a5ad9654c1f0e57440e30afd92846a5", "mdi.html": "c92bd28c434865d6cabb34cd3c0a3e4c",
"micromarkdown-js.html": "93b5ec4016f0bba585521cf4d18dec1a", "micromarkdown-js.html": "93b5ec4016f0bba585521cf4d18dec1a",
"panels/ha-panel-automation.html": "21cba0a4fee9d2b45dda47f7a1dd82d8", "panels/ha-panel-automation.html": "4f98839bb082885657bbcd0ac04fc680",
"panels/ha-panel-config.html": "59d9eb28758b497a4d9b2428f978b9b1", "panels/ha-panel-config.html": "76853de505d173e82249bf605eb73505",
"panels/ha-panel-dev-event.html": "2db9c218065ef0f61d8d08db8093cad2", "panels/ha-panel-dev-event.html": "4886c821235492b1b92739b580d21c61",
"panels/ha-panel-dev-info.html": "61610e015a411cfc84edd2c4d489e71d", "panels/ha-panel-dev-info.html": "24e888ec7a8acd0c395b34396e9001bc",
"panels/ha-panel-dev-service.html": "415552027cb083badeff5f16080410ed", "panels/ha-panel-dev-service.html": "92c6be30b1af95791d5a6429df505852",
"panels/ha-panel-dev-state.html": "d70314913b8923d750932367b1099750", "panels/ha-panel-dev-state.html": "8f1a27c04db6329d31cfcc7d0d6a0869",
"panels/ha-panel-dev-template.html": "567fbf86735e1b891e40c2f4060fec9b", "panels/ha-panel-dev-template.html": "d33a55b937b50cdfe8b6fae81f70a139",
"panels/ha-panel-hassio.html": "9474ba65077371622f21ed9a30cf5229", "panels/ha-panel-hassio.html": "9474ba65077371622f21ed9a30cf5229",
"panels/ha-panel-history.html": "89062c48c76206cad1cec14ddbb1cbb1", "panels/ha-panel-history.html": "35177e2046c9a4191c8f51f8160255ce",
"panels/ha-panel-iframe.html": "d920f0aa3c903680f2f8795e2255daab", "panels/ha-panel-iframe.html": "d920f0aa3c903680f2f8795e2255daab",
"panels/ha-panel-logbook.html": "6dd6a16f52117318b202e60f98400163", "panels/ha-panel-logbook.html": "7c45bd41c146ec38b9938b8a5188bb0d",
"panels/ha-panel-map.html": "31c592c239636f91e07c7ac232a5ebc4", "panels/ha-panel-map.html": "0ba605729197c4724ecc7310b08f7050",
"panels/ha-panel-zwave.html": "92edac58dd52c297c761fd9acec7f436", "panels/ha-panel-zwave.html": "2ea2223339d1d2faff478751c2927d11",
"websocket_test.html": "575de64b431fe11c3785bf96d7813450" "websocket_test.html": "575de64b431fe11c3785bf96d7813450"
} }

File diff suppressed because one or more lines are too long

@ -1 +1 @@
Subproject commit 81ab4ff8a8ef7cc4b96b60f63c16472b0427adc7 Subproject commit 1ad42592134c290119879e8f8505ef5736a3071e

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
<html><head><meta charset="UTF-8"></head><body><dom-module id="ha-panel-dev-info"><template><style include="iron-positioning ha-style">:host{-ms-user-select:initial;-webkit-user-select:initial;-moz-user-select:initial}.content{padding:16px}.about{text-align:center;line-height:2em}.version{@apply(--paper-font-headline)}.develop{@apply(--paper-font-subhead)}.about a{color:var(--dark-primary-color)}.error-log-intro{margin-top:16px;border-top:1px solid var(--light-primary-color);padding-top:16px}paper-icon-button{float:right}.error-log{white-space:pre-wrap}</style><app-header-layout has-scrolling-region=""><app-header fixed=""><app-toolbar><ha-menu-button narrow="[[narrow]]" show-menu="[[showMenu]]"></ha-menu-button><div main-title="">About</div></app-toolbar></app-header><div class="content fit"><div class="about"><p class="version"><a href="https://home-assistant.io"><img src="/static/icons/favicon-192x192.png" height="192"></a><br>Home Assistant<br>[[hass.config.core.version]]</p><p>Path to configuration.yaml: [[hass.config.core.config_dir]]</p><p class="develop"><a href="https://home-assistant.io/developers/credits/" target="_blank">Developed by a bunch of awesome people.</a></p><p>Published under the Apache 2.0 license<br>Source: <a href="https://github.com/home-assistant/home-assistant" target="_blank">server</a><a href="https://github.com/home-assistant/home-assistant-polymer" target="_blank">frontend-ui</a></p><p>Built using <a href="https://www.python.org">Python 3</a>, <a href="https://www.polymer-project.org" target="_blank">Polymer [[polymerVersion]]</a>, Icons by <a href="https://www.google.com/design/icons/" target="_blank">Google</a> and <a href="https://MaterialDesignIcons.com" target="_blank">MaterialDesignIcons.com</a>.</p></div><p class="error-log-intro">The following errors have been logged this session:<paper-icon-button icon="mdi:refresh" on-tap="refreshErrorLog"></paper-icon-button></p><div class="error-log">[[errorLog]]</div></div></app-header-layout></template></dom-module><script>Polymer({is:"ha-panel-dev-info",properties:{hass:{type:Object},narrow:{type:Boolean,value:!1},showMenu:{type:Boolean,value:!1},polymerVersion:{type:String,value:Polymer.version},errorLog:{type:String,value:""}},attached:function(){this.refreshErrorLog()},refreshErrorLog:function(e){e&&e.preventDefault(),this.errorLog="Loading error log…",this.hass.callApi("GET","error_log").then(function(e){this.errorLog=e||"No errors have been reported."}.bind(this))}})</script></body></html> <html><head><meta charset="UTF-8"></head><body><dom-module id="ha-panel-dev-info"><template><style include="iron-positioning ha-style">:host{-ms-user-select:initial;-webkit-user-select:initial;-moz-user-select:initial}.content{padding:16px}.about{text-align:center;line-height:2em}.version{@apply(--paper-font-headline)}.develop{@apply(--paper-font-subhead)}.about a{color:var(--dark-primary-color)}.error-log-intro{margin-top:16px;border-top:1px solid var(--light-primary-color);padding-top:16px}paper-icon-button{float:right}.error-log{white-space:pre-wrap}</style><app-header-layout has-scrolling-region=""><app-header slot="header" fixed=""><app-toolbar><ha-menu-button narrow="[[narrow]]" show-menu="[[showMenu]]"></ha-menu-button><div main-title="">About</div></app-toolbar></app-header><div class="content fit"><div class="about"><p class="version"><a href="https://home-assistant.io"><img src="/static/icons/favicon-192x192.png" height="192"></a><br>Home Assistant<br>[[hass.config.core.version]]</p><p>Path to configuration.yaml: [[hass.config.core.config_dir]]</p><p class="develop"><a href="https://home-assistant.io/developers/credits/" target="_blank">Developed by a bunch of awesome people.</a></p><p>Published under the Apache 2.0 license<br>Source: <a href="https://github.com/home-assistant/home-assistant" target="_blank">server</a><a href="https://github.com/home-assistant/home-assistant-polymer" target="_blank">frontend-ui</a></p><p>Built using <a href="https://www.python.org">Python 3</a>, <a href="https://www.polymer-project.org" target="_blank">Polymer [[polymerVersion]]</a>, Icons by <a href="https://www.google.com/design/icons/" target="_blank">Google</a> and <a href="https://MaterialDesignIcons.com" target="_blank">MaterialDesignIcons.com</a>.</p></div><p class="error-log-intro">The following errors have been logged this session:<paper-icon-button icon="mdi:refresh" on-tap="refreshErrorLog"></paper-icon-button></p><div class="error-log">[[errorLog]]</div></div></app-header-layout></template></dom-module><script>Polymer({is:"ha-panel-dev-info",properties:{hass:{type:Object},narrow:{type:Boolean,value:!1},showMenu:{type:Boolean,value:!1},polymerVersion:{type:String,value:Polymer.version},errorLog:{type:String,value:""}},attached:function(){this.refreshErrorLog()},refreshErrorLog:function(e){e&&e.preventDefault(),this.errorLog="Loading error log…",this.hass.callApi("GET","error_log").then(function(e){this.errorLog=e||"No errors have been reported."}.bind(this))}})</script></body></html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,55 +0,0 @@
"""
Support for Insteon Hub.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/insteon_hub/
"""
import logging
import voluptuous as vol
from homeassistant.const import (CONF_API_KEY, CONF_PASSWORD, CONF_USERNAME)
from homeassistant.helpers import discovery
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['insteon_hub==0.4.5']
_LOGGER = logging.getLogger(__name__)
DOMAIN = 'insteon_hub'
INSTEON = None
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_API_KEY): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_USERNAME): cv.string,
})
}, extra=vol.ALLOW_EXTRA)
def setup(hass, config):
"""Set up the Insteon Hub component.
This will automatically import associated lights.
"""
_LOGGER.warning("Component disabled at request from Insteon. "
"For more information: https://goo.gl/zLJaic")
return False
# pylint: disable=unreachable
import insteon
username = config[DOMAIN][CONF_USERNAME]
password = config[DOMAIN][CONF_PASSWORD]
api_key = config[DOMAIN][CONF_API_KEY]
global INSTEON
INSTEON = insteon.Insteon(username, password, api_key)
if INSTEON is None:
_LOGGER.error("Could not connect to Insteon service")
return False
discovery.load_platform(hass, 'light', DOMAIN, {}, config)
return True

View File

@ -51,7 +51,7 @@ def setup(hass, config):
res = KNXTUNNEL.connect() res = KNXTUNNEL.connect()
_LOGGER.debug("Res = %s", res) _LOGGER.debug("Res = %s", res)
if not res: if not res:
_LOGGER.exception("Could not connect to KNX/IP interface %s", host) _LOGGER.error("Could not connect to KNX/IP interface %s", host)
return False return False
except KNXException as ex: except KNXException as ex:
@ -127,7 +127,10 @@ class KNXGroupAddress(Entity):
self._config = config self._config = config
self._state = False self._state = False
self._data = None self._data = None
_LOGGER.debug("Initalizing KNX group address %s", self.address) _LOGGER.debug(
"Initalizing KNX group address for %s (%s)",
self.name, self.address
)
def handle_knx_message(addr, data): def handle_knx_message(addr, data):
"""Handle an incoming KNX frame. """Handle an incoming KNX frame.
@ -198,11 +201,15 @@ class KNXGroupAddress(Entity):
self._data = res self._data = res
else: else:
_LOGGER.debug( _LOGGER.debug(
"Unable to read from KNX address: %s (None)", self.address) "%s: unable to read from KNX address: %s (None)",
self.name, self.address
)
except KNXException: except KNXException:
_LOGGER.exception( _LOGGER.exception(
"Unable to read from KNX address: %s", self.address) "%s: unable to read from KNX address: %s",
self.name, self.address
)
return False return False
@ -213,9 +220,6 @@ class KNXMultiAddressDevice(Entity):
to be controlled by multiple group addresses. to be controlled by multiple group addresses.
""" """
names = {}
values = {}
def __init__(self, hass, config, required, optional=None): def __init__(self, hass, config, required, optional=None):
"""Initialize the device. """Initialize the device.
@ -226,33 +230,69 @@ class KNXMultiAddressDevice(Entity):
""" """
from knxip.core import parse_group_address, KNXException from knxip.core import parse_group_address, KNXException
self.names = {}
self.values = {}
self._config = config self._config = config
self._state = False self._state = False
self._data = None self._data = None
_LOGGER.debug("Initalizing KNX multi address device") _LOGGER.debug(
"%s: initalizing KNX multi address device",
self.name
)
settings = self._config.config
if config.address:
_LOGGER.debug(
"%s: base address: address=%s",
self.name, settings.get('address')
)
self.names[config.address] = 'base'
if config.state_address:
_LOGGER.debug(
"%s, state address: state_address=%s",
self.name, settings.get('state_address')
)
self.names[config.state_address] = 'state'
# parse required addresses # parse required addresses
for name in required: for name in required:
_LOGGER.info(name)
paramname = '{}{}'.format(name, '_address') paramname = '{}{}'.format(name, '_address')
addr = self._config.config.get(paramname) addr = settings.get(paramname)
if addr is None: if addr is None:
_LOGGER.exception( _LOGGER.error(
"Required KNX group address %s missing", paramname) "%s: Required KNX group address %s missing",
self.name, paramname
)
raise KNXException( raise KNXException(
"Group address for %s missing in configuration", paramname) "%s: Group address for {} missing in "
"configuration for {}".format(
self.name, paramname
)
)
_LOGGER.debug(
"%s: (required parameter) %s=%s",
self.name, paramname, addr
)
addr = parse_group_address(addr) addr = parse_group_address(addr)
self.names[addr] = name self.names[addr] = name
# parse optional addresses # parse optional addresses
for name in optional: for name in optional:
paramname = '{}{}'.format(name, '_address') paramname = '{}{}'.format(name, '_address')
addr = self._config.config.get(paramname) addr = settings.get(paramname)
_LOGGER.debug(
"%s: (optional parameter) %s=%s",
self.name, paramname, addr
)
if addr: if addr:
try: try:
addr = parse_group_address(addr) addr = parse_group_address(addr)
except KNXException: except KNXException:
_LOGGER.exception("Cannot parse group address %s", addr) _LOGGER.exception(
"%s: cannot parse group address %s",
self.name, addr
)
self.names[addr] = name self.names[addr] = name
@property @property
@ -280,11 +320,53 @@ class KNXMultiAddressDevice(Entity):
This is mostly important for optional addresses. This is mostly important for optional addresses.
""" """
for attributename, dummy_attribute in self.names.items(): for attributename in self.names.values():
if attributename == name: if attributename == name:
return True return True
return False return False
def set_percentage(self, name, percentage):
"""Set a percentage in knx for a given attribute.
DPT_Scaling / DPT 5.001 is a single byte scaled percentage
"""
percentage = abs(percentage) # only accept positive values
scaled_value = percentage * 255 / 100
value = min(255, scaled_value)
return self.set_int_value(name, value)
def get_percentage(self, name):
"""Get a percentage from knx for a given attribute.
DPT_Scaling / DPT 5.001 is a single byte scaled percentage
"""
value = self.get_int_value(name)
percentage = round(value * 100 / 255)
return percentage
def set_int_value(self, name, value, num_bytes=1):
"""Set an integer value for a given attribute."""
# KNX packets are big endian
value = round(value) # only accept integers
b_value = value.to_bytes(num_bytes, byteorder='big')
return self.set_value(name, list(b_value))
def get_int_value(self, name):
"""Get an integer value for a given attribute."""
# KNX packets are big endian
summed_value = 0
raw_value = self.value(name)
try:
# convert raw value in bytes
for val in raw_value:
summed_value *= 256
summed_value += val
except TypeError:
# pknx returns a non-iterable type for unsuccessful reads
pass
return summed_value
def value(self, name): def value(self, name):
"""Return the value to a given named attribute.""" """Return the value to a given named attribute."""
from knxip.core import KNXException from knxip.core import KNXException
@ -295,13 +377,21 @@ class KNXMultiAddressDevice(Entity):
addr = attributeaddress addr = attributeaddress
if addr is None: if addr is None:
_LOGGER.exception("Attribute %s undefined", name) _LOGGER.error("%s: attribute '%s' undefined",
self.name, name)
_LOGGER.debug(
"%s: defined attributes: %s",
self.name, str(self.names)
)
return False return False
try: try:
res = KNXTUNNEL.group_read(addr, use_cache=self.cache) res = KNXTUNNEL.group_read(addr, use_cache=self.cache)
except KNXException: except KNXException:
_LOGGER.exception("Unable to read from KNX address: %s", addr) _LOGGER.exception(
"%s: unable to read from KNX address: %s",
self.name, addr
)
return False return False
return res return res
@ -316,13 +406,21 @@ class KNXMultiAddressDevice(Entity):
addr = attributeaddress addr = attributeaddress
if addr is None: if addr is None:
_LOGGER.exception("Attribute %s undefined", name) _LOGGER.error("%s: attribute '%s' undefined",
self.name, name)
_LOGGER.debug(
"%s: defined attributes: %s",
self.name, str(self.names)
)
return False return False
try: try:
KNXTUNNEL.group_write(addr, value) KNXTUNNEL.group_write(addr, value)
except KNXException: except KNXException:
_LOGGER.exception("Unable to write to KNX address: %s", addr) _LOGGER.exception(
"%s: unable to write to KNX address: %s",
self.name, addr
)
return False return False
return True return True

View File

@ -14,7 +14,7 @@ from homeassistant.components.light import (
PLATFORM_SCHEMA) PLATFORM_SCHEMA)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['decora==0.4'] REQUIREMENTS = ['decora==0.6']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -59,7 +59,7 @@ class DecoraLight(Light):
self._switch = decora.decora(self._address, self._key) self._switch = decora.decora(self._address, self._key)
self._switch.connect() self._switch.connect()
self._state = self._switch.get_on() self._state = self._switch.get_on()
self._brightness = self._switch.get_brightness() self._brightness = self._switch.get_brightness() * 2.55
self.is_valid = True self.is_valid = True
@property @property
@ -99,7 +99,7 @@ class DecoraLight(Light):
def set_state(self, brightness): def set_state(self, brightness):
"""Set the state of this lamp to the provided brightness.""" """Set the state of this lamp to the provided brightness."""
self._switch.set_brightness(brightness) self._switch.set_brightness(int(brightness / 2.55))
self._brightness = brightness self._brightness = brightness
return True return True
@ -120,5 +120,5 @@ class DecoraLight(Light):
def update(self): def update(self):
"""Synchronise internal state with the actual light state.""" """Synchronise internal state with the actual light state."""
self._brightness = self._switch.get_brightness() self._brightness = self._switch.get_brightness() * 2.55
self._state = self._switch.get_on() self._state = self._switch.get_on()

View File

@ -1,79 +0,0 @@
"""
Support for Insteon Hub lights.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/insteon_hub/
"""
from homeassistant.components.insteon_hub import INSTEON
from homeassistant.components.light import (ATTR_BRIGHTNESS,
SUPPORT_BRIGHTNESS, Light)
DEPENDENCIES = ['insteon_hub']
SUPPORT_INSTEON_HUB = SUPPORT_BRIGHTNESS
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Insteon Hub light platform."""
devs = []
for device in INSTEON.devices:
if device.DeviceCategory == "Switched Lighting Control":
devs.append(InsteonToggleDevice(device))
if device.DeviceCategory == "Dimmable Lighting Control":
devs.append(InsteonToggleDevice(device))
add_devices(devs)
class InsteonToggleDevice(Light):
"""An abstract Class for an Insteon node."""
def __init__(self, node):
"""Initialize the device."""
self.node = node
self._value = 0
@property
def name(self):
"""Return the the name of the node."""
return self.node.DeviceName
@property
def unique_id(self):
"""Return the ID of this insteon node."""
return self.node.DeviceID
@property
def brightness(self):
"""Return the brightness of this light between 0..255."""
return self._value / 100 * 255
def update(self):
"""Update state of the sensor."""
resp = self.node.send_command('get_status', wait=True)
try:
self._value = resp['response']['level']
except KeyError:
pass
@property
def is_on(self):
"""Return the boolean response if the node is on."""
return self._value != 0
@property
def supported_features(self):
"""Flag supported features."""
return SUPPORT_INSTEON_HUB
def turn_on(self, **kwargs):
"""Turn device on."""
if ATTR_BRIGHTNESS in kwargs:
self._value = kwargs[ATTR_BRIGHTNESS] / 255 * 100
self.node.send_command('on', self._value)
else:
self._value = 100
self.node.send_command('on')
def turn_off(self, **kwargs):
"""Turn device off."""
self.node.send_command('off')

View File

@ -56,7 +56,7 @@ class ISYLightDevice(isy.ISYDevice, Light):
def turn_off(self, **kwargs) -> None: def turn_off(self, **kwargs) -> None:
"""Send the turn off command to the ISY994 light device.""" """Send the turn off command to the ISY994 light device."""
if not self._node.off(): if not self._node.off():
_LOGGER.debug("Unable to turn on light") _LOGGER.debug("Unable to turn off light")
def turn_on(self, brightness=None, **kwargs) -> None: def turn_on(self, brightness=None, **kwargs) -> None:
"""Send the turn on command to the ISY994 light device.""" """Send the turn on command to the ISY994 light device."""

View File

@ -11,18 +11,19 @@ import math
from os import path from os import path
from functools import partial from functools import partial
from datetime import timedelta from datetime import timedelta
import async_timeout
import voluptuous as vol import voluptuous as vol
from homeassistant.components.light import ( from homeassistant.components.light import (
Light, DOMAIN, PLATFORM_SCHEMA, LIGHT_TURN_ON_SCHEMA, Light, DOMAIN, PLATFORM_SCHEMA, LIGHT_TURN_ON_SCHEMA,
ATTR_BRIGHTNESS, ATTR_RGB_COLOR, ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, ATTR_COLOR_NAME, ATTR_RGB_COLOR,
ATTR_XY_COLOR, ATTR_COLOR_TEMP, ATTR_TRANSITION, ATTR_EFFECT, ATTR_XY_COLOR, ATTR_COLOR_TEMP, ATTR_KELVIN, ATTR_TRANSITION, ATTR_EFFECT,
SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_RGB_COLOR, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_RGB_COLOR,
SUPPORT_XY_COLOR, SUPPORT_TRANSITION, SUPPORT_EFFECT, SUPPORT_XY_COLOR, SUPPORT_TRANSITION, SUPPORT_EFFECT,
VALID_BRIGHTNESS, VALID_BRIGHTNESS_PCT,
preprocess_turn_on_alternatives) preprocess_turn_on_alternatives)
from homeassistant.config import load_yaml_config_file from homeassistant.config import load_yaml_config_file
from homeassistant.const import ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_STOP
from homeassistant import util from homeassistant import util
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.helpers.event import async_track_point_in_utc_time
@ -30,34 +31,79 @@ from homeassistant.helpers.service import extract_entity_ids
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
import homeassistant.util.color as color_util import homeassistant.util.color as color_util
from . import effects as lifx_effects
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['aiolifx==0.4.8'] REQUIREMENTS = ['aiolifx==0.5.0', 'aiolifx_effects==0.1.0']
UDP_BROADCAST_PORT = 56700 UDP_BROADCAST_PORT = 56700
# Delay (in ms) expected for changes to take effect in the physical bulb
BULB_LATENCY = 500
CONF_SERVER = 'server' CONF_SERVER = 'server'
SERVICE_LIFX_SET_STATE = 'lifx_set_state'
ATTR_HSBK = 'hsbk'
ATTR_INFRARED = 'infrared'
ATTR_POWER = 'power'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_SERVER, default='0.0.0.0'): cv.string, vol.Optional(CONF_SERVER, default='0.0.0.0'): cv.string,
}) })
SERVICE_LIFX_SET_STATE = 'lifx_set_state'
ATTR_INFRARED = 'infrared'
ATTR_POWER = 'power'
LIFX_SET_STATE_SCHEMA = LIGHT_TURN_ON_SCHEMA.extend({ LIFX_SET_STATE_SCHEMA = LIGHT_TURN_ON_SCHEMA.extend({
ATTR_INFRARED: vol.All(vol.Coerce(int), vol.Clamp(min=0, max=255)), ATTR_INFRARED: vol.All(vol.Coerce(int), vol.Clamp(min=0, max=255)),
ATTR_POWER: cv.boolean, ATTR_POWER: cv.boolean,
}) })
SERVICE_EFFECT_PULSE = 'lifx_effect_pulse'
SERVICE_EFFECT_COLORLOOP = 'lifx_effect_colorloop'
SERVICE_EFFECT_STOP = 'lifx_effect_stop'
ATTR_POWER_ON = 'power_on'
ATTR_MODE = 'mode'
ATTR_PERIOD = 'period'
ATTR_CYCLES = 'cycles'
ATTR_SPREAD = 'spread'
ATTR_CHANGE = 'change'
PULSE_MODE_BLINK = 'blink'
PULSE_MODE_BREATHE = 'breathe'
PULSE_MODE_PING = 'ping'
PULSE_MODE_STROBE = 'strobe'
PULSE_MODE_SOLID = 'solid'
PULSE_MODES = [PULSE_MODE_BLINK, PULSE_MODE_BREATHE, PULSE_MODE_PING,
PULSE_MODE_STROBE, PULSE_MODE_SOLID]
LIFX_EFFECT_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
vol.Optional(ATTR_POWER_ON, default=True): cv.boolean,
})
LIFX_EFFECT_PULSE_SCHEMA = LIFX_EFFECT_SCHEMA.extend({
ATTR_BRIGHTNESS: VALID_BRIGHTNESS,
ATTR_BRIGHTNESS_PCT: VALID_BRIGHTNESS_PCT,
ATTR_COLOR_NAME: cv.string,
ATTR_RGB_COLOR: vol.All(vol.ExactSequence((cv.byte, cv.byte, cv.byte)),
vol.Coerce(tuple)),
ATTR_COLOR_TEMP: vol.All(vol.Coerce(int), vol.Range(min=1)),
ATTR_KELVIN: vol.All(vol.Coerce(int), vol.Range(min=0)),
ATTR_PERIOD: vol.All(vol.Coerce(float), vol.Range(min=0.05)),
ATTR_CYCLES: vol.All(vol.Coerce(float), vol.Range(min=1)),
ATTR_MODE: vol.In(PULSE_MODES),
})
LIFX_EFFECT_COLORLOOP_SCHEMA = LIFX_EFFECT_SCHEMA.extend({
ATTR_BRIGHTNESS: VALID_BRIGHTNESS,
ATTR_BRIGHTNESS_PCT: VALID_BRIGHTNESS_PCT,
ATTR_PERIOD: vol.All(vol.Coerce(float), vol.Clamp(min=0.05)),
ATTR_CHANGE: vol.All(vol.Coerce(float), vol.Clamp(min=0, max=360)),
ATTR_SPREAD: vol.All(vol.Coerce(float), vol.Clamp(min=0, max=360)),
ATTR_TRANSITION: vol.All(vol.Coerce(float), vol.Range(min=0)),
})
LIFX_EFFECT_STOP_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
})
@asyncio.coroutine @asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None): def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
@ -71,27 +117,79 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
server_addr = config.get(CONF_SERVER) server_addr = config.get(CONF_SERVER)
lifx_manager = LIFXManager(hass, async_add_devices) lifx_manager = LIFXManager(hass, async_add_devices)
lifx_discovery = aiolifx.LifxDiscovery(hass.loop, lifx_manager)
coro = hass.loop.create_datagram_endpoint( coro = hass.loop.create_datagram_endpoint(
partial(aiolifx.LifxDiscovery, hass.loop, lifx_manager), lambda: lifx_discovery, local_addr=(server_addr, UDP_BROADCAST_PORT))
local_addr=(server_addr, UDP_BROADCAST_PORT))
hass.async_add_job(coro) hass.async_add_job(coro)
lifx_effects.setup(hass, lifx_manager) @callback
def cleanup(event):
"""Clean up resources."""
lifx_discovery.cleanup()
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, cleanup)
return True return True
def find_hsbk(**kwargs):
"""Find the desired color from a number of possible inputs."""
hue, saturation, brightness, kelvin = [None]*4
preprocess_turn_on_alternatives(kwargs)
if ATTR_RGB_COLOR in kwargs:
hue, saturation, brightness = \
color_util.color_RGB_to_hsv(*kwargs[ATTR_RGB_COLOR])
saturation = convert_8_to_16(saturation)
brightness = convert_8_to_16(brightness)
kelvin = 3500
if ATTR_XY_COLOR in kwargs:
hue, saturation = color_util.color_xy_to_hs(*kwargs[ATTR_XY_COLOR])
saturation = convert_8_to_16(saturation)
kelvin = 3500
if ATTR_COLOR_TEMP in kwargs:
kelvin = int(color_util.color_temperature_mired_to_kelvin(
kwargs[ATTR_COLOR_TEMP]))
saturation = 0
if ATTR_BRIGHTNESS in kwargs:
brightness = convert_8_to_16(kwargs[ATTR_BRIGHTNESS])
hsbk = [hue, saturation, brightness, kelvin]
return None if hsbk == [None]*4 else hsbk
def merge_hsbk(base, change):
"""Copy change on top of base, except when None."""
if change is None:
return None
return list(map(lambda x, y: y if y is not None else x, base, change))
class LIFXManager(object): class LIFXManager(object):
"""Representation of all known LIFX entities.""" """Representation of all known LIFX entities."""
def __init__(self, hass, async_add_devices): def __init__(self, hass, async_add_devices):
"""Initialize the light.""" """Initialize the light."""
import aiolifx_effects
self.entities = {} self.entities = {}
self.hass = hass self.hass = hass
self.async_add_devices = async_add_devices self.async_add_devices = async_add_devices
self.effects_conductor = aiolifx_effects.Conductor(loop=hass.loop)
descriptions = load_yaml_config_file(
path.join(path.dirname(__file__), 'services.yaml'))
self.register_set_state(descriptions)
self.register_effects(descriptions)
def register_set_state(self, descriptions):
"""Register the LIFX set_state service call."""
@asyncio.coroutine @asyncio.coroutine
def async_service_handle(service): def async_service_handle(service):
"""Apply a service.""" """Apply a service."""
@ -99,22 +197,73 @@ class LIFXManager(object):
for light in self.service_to_entities(service): for light in self.service_to_entities(service):
if service.service == SERVICE_LIFX_SET_STATE: if service.service == SERVICE_LIFX_SET_STATE:
task = light.async_set_state(**service.data) task = light.async_set_state(**service.data)
tasks.append(hass.async_add_job(task)) tasks.append(self.hass.async_add_job(task))
if tasks: if tasks:
yield from asyncio.wait(tasks, loop=hass.loop) yield from asyncio.wait(tasks, loop=self.hass.loop)
descriptions = self.get_descriptions() self.hass.services.async_register(
hass.services.async_register(
DOMAIN, SERVICE_LIFX_SET_STATE, async_service_handle, DOMAIN, SERVICE_LIFX_SET_STATE, async_service_handle,
descriptions.get(SERVICE_LIFX_SET_STATE), descriptions.get(SERVICE_LIFX_SET_STATE),
schema=LIFX_SET_STATE_SCHEMA) schema=LIFX_SET_STATE_SCHEMA)
@staticmethod def register_effects(self, descriptions):
def get_descriptions(): """Register the LIFX effects as hass service calls."""
"""Load and return descriptions for our own service calls.""" @asyncio.coroutine
return load_yaml_config_file( def async_service_handle(service):
path.join(path.dirname(__file__), 'services.yaml')) """Apply a service, i.e. start an effect."""
entities = self.service_to_entities(service)
if entities:
yield from self.start_effect(
entities, service.service, **service.data)
self.hass.services.async_register(
DOMAIN, SERVICE_EFFECT_PULSE, async_service_handle,
descriptions.get(SERVICE_EFFECT_PULSE),
schema=LIFX_EFFECT_PULSE_SCHEMA)
self.hass.services.async_register(
DOMAIN, SERVICE_EFFECT_COLORLOOP, async_service_handle,
descriptions.get(SERVICE_EFFECT_COLORLOOP),
schema=LIFX_EFFECT_COLORLOOP_SCHEMA)
self.hass.services.async_register(
DOMAIN, SERVICE_EFFECT_STOP, async_service_handle,
descriptions.get(SERVICE_EFFECT_STOP),
schema=LIFX_EFFECT_STOP_SCHEMA)
@asyncio.coroutine
def start_effect(self, entities, service, **kwargs):
"""Start a light effect on entities."""
import aiolifx_effects
devices = list(map(lambda l: l.device, entities))
if service == SERVICE_EFFECT_PULSE:
effect = aiolifx_effects.EffectPulse(
power_on=kwargs.get(ATTR_POWER_ON),
period=kwargs.get(ATTR_PERIOD),
cycles=kwargs.get(ATTR_CYCLES),
mode=kwargs.get(ATTR_MODE),
hsbk=find_hsbk(**kwargs),
)
yield from self.effects_conductor.start(effect, devices)
elif service == SERVICE_EFFECT_COLORLOOP:
preprocess_turn_on_alternatives(kwargs)
brightness = None
if ATTR_BRIGHTNESS in kwargs:
brightness = convert_8_to_16(kwargs[ATTR_BRIGHTNESS])
effect = aiolifx_effects.EffectColorloop(
power_on=kwargs.get(ATTR_POWER_ON),
period=kwargs.get(ATTR_PERIOD),
change=kwargs.get(ATTR_CHANGE),
spread=kwargs.get(ATTR_SPREAD),
transition=kwargs.get(ATTR_TRANSITION),
brightness=brightness,
)
yield from self.effects_conductor.start(effect, devices)
elif service == SERVICE_EFFECT_STOP:
yield from self.effects_conductor.stop(devices)
def service_to_entities(self, service): def service_to_entities(self, service):
"""Return the known devices that a service call mentions.""" """Return the known devices that a service call mentions."""
@ -148,7 +297,7 @@ class LIFXManager(object):
@callback @callback
def ready(self, device, msg): def ready(self, device, msg):
"""Handle the device once all data is retrieved.""" """Handle the device once all data is retrieved."""
entity = LIFXLight(device) entity = LIFXLight(device, self.effects_conductor)
_LOGGER.debug("%s register READY", entity.who) _LOGGER.debug("%s register READY", entity.who)
self.entities[device.mac_addr] = entity self.entities[device.mac_addr] = entity
self.async_add_devices([entity]) self.async_add_devices([entity])
@ -182,17 +331,13 @@ class AwaitAioLIFX:
@asyncio.coroutine @asyncio.coroutine
def wait(self, method): def wait(self, method):
"""Call an aiolifx method and wait for its response or a timeout.""" """Call an aiolifx method and wait for its response."""
self.device = None
self.message = None
self.event.clear() self.event.clear()
method(self.callback) method(callb=self.callback)
while self.light.available and not self.event.is_set():
try:
with async_timeout.timeout(1.0, loop=self.light.hass.loop):
yield from self.event.wait()
except asyncio.TimeoutError:
pass
yield from self.event.wait()
return self.message return self.message
@ -209,17 +354,13 @@ def convert_16_to_8(value):
class LIFXLight(Light): class LIFXLight(Light):
"""Representation of a LIFX light.""" """Representation of a LIFX light."""
def __init__(self, device): def __init__(self, device, effects_conductor):
"""Initialize the light.""" """Initialize the light."""
self.device = device self.device = device
self.effects_conductor = effects_conductor
self.registered = True self.registered = True
self.product = device.product self.product = device.product
self.blocker = None
self.effect_data = None
self.postponed_update = None self.postponed_update = None
self._name = device.label
self.set_power(device.power_level)
self.set_color(*device.color)
@property @property
def lifxwhite(self): def lifxwhite(self):
@ -235,34 +376,33 @@ class LIFXLight(Light):
@property @property
def name(self): def name(self):
"""Return the name of the device.""" """Return the name of the device."""
return self._name return self.device.label
@property @property
def who(self): def who(self):
"""Return a string identifying the device.""" """Return a string identifying the device."""
ip_addr = '-' return "%s (%s)" % (self.device.ip_addr, self.name)
if self.device:
ip_addr = self.device.ip_addr[0]
return "%s (%s)" % (ip_addr, self.name)
@property @property
def rgb_color(self): def rgb_color(self):
"""Return the RGB value.""" """Return the RGB value."""
_LOGGER.debug( hue, sat, bri, _ = self.device.color
"rgb_color: [%d %d %d]", self._rgb[0], self._rgb[1], self._rgb[2])
return self._rgb return color_util.color_hsv_to_RGB(
hue, convert_16_to_8(sat), convert_16_to_8(bri))
@property @property
def brightness(self): def brightness(self):
"""Return the brightness of this light between 0..255.""" """Return the brightness of this light between 0..255."""
brightness = convert_16_to_8(self._bri) brightness = convert_16_to_8(self.device.color[2])
_LOGGER.debug("brightness: %d", brightness) _LOGGER.debug("brightness: %d", brightness)
return brightness return brightness
@property @property
def color_temp(self): def color_temp(self):
"""Return the color temperature.""" """Return the color temperature."""
temperature = color_util.color_temperature_kelvin_to_mired(self._kel) kelvin = self.device.color[3]
temperature = color_util.color_temperature_kelvin_to_mired(kelvin)
_LOGGER.debug("color_temp: %d", temperature) _LOGGER.debug("color_temp: %d", temperature)
return temperature return temperature
@ -290,13 +430,15 @@ class LIFXLight(Light):
@property @property
def is_on(self): def is_on(self):
"""Return true if device is on.""" """Return true if device is on."""
_LOGGER.debug("is_on: %d", self._power) return self.device.power_level != 0
return self._power != 0
@property @property
def effect(self): def effect(self):
"""Return the currently running effect.""" """Return the name of the currently running effect."""
return self.effect_data.effect.name if self.effect_data else None effect = self.effects_conductor.effect(self.device)
if effect:
return 'lifx_effect_' + effect.name
return None
@property @property
def supported_features(self): def supported_features(self):
@ -311,38 +453,35 @@ class LIFXLight(Light):
@property @property
def effect_list(self): def effect_list(self):
"""Return the list of supported effects.""" """Return the list of supported effects for this light."""
return lifx_effects.effect_list(self) if self.lifxwhite:
return [
SERVICE_EFFECT_PULSE,
SERVICE_EFFECT_STOP,
]
return [
SERVICE_EFFECT_COLORLOOP,
SERVICE_EFFECT_PULSE,
SERVICE_EFFECT_STOP,
]
@asyncio.coroutine @asyncio.coroutine
def update_after_transition(self, now): def update_after_transition(self, now):
"""Request new status after completion of the last transition.""" """Request new status after completion of the last transition."""
self.postponed_update = None self.postponed_update = None
yield from self.refresh_state() yield from self.async_update()
yield from self.async_update_ha_state()
@asyncio.coroutine
def unblock_updates(self, now):
"""Allow async_update after the new state has settled on the bulb."""
self.blocker = None
yield from self.refresh_state()
yield from self.async_update_ha_state() yield from self.async_update_ha_state()
def update_later(self, when): def update_later(self, when):
"""Block immediate update requests and schedule one for later.""" """Schedule an update requests when a transition is over."""
if self.blocker:
self.blocker()
self.blocker = async_track_point_in_utc_time(
self.hass, self.unblock_updates,
util.dt.utcnow() + timedelta(milliseconds=BULB_LATENCY))
if self.postponed_update: if self.postponed_update:
self.postponed_update() self.postponed_update()
self.postponed_update = None self.postponed_update = None
if when > BULB_LATENCY: if when > 0:
self.postponed_update = async_track_point_in_utc_time( self.postponed_update = async_track_point_in_utc_time(
self.hass, self.update_after_transition, self.hass, self.update_after_transition,
util.dt.utcnow() + timedelta(milliseconds=when+BULB_LATENCY)) util.dt.utcnow() + timedelta(milliseconds=when))
@asyncio.coroutine @asyncio.coroutine
def async_turn_on(self, **kwargs): def async_turn_on(self, **kwargs):
@ -359,10 +498,10 @@ class LIFXLight(Light):
@asyncio.coroutine @asyncio.coroutine
def async_set_state(self, **kwargs): def async_set_state(self, **kwargs):
"""Set a color on the light and turn it on/off.""" """Set a color on the light and turn it on/off."""
yield from self.stop_effect() yield from self.effects_conductor.stop([self.device])
if ATTR_EFFECT in kwargs: if ATTR_EFFECT in kwargs:
yield from lifx_effects.default_effect(self, **kwargs) yield from self.default_effect(**kwargs)
return return
if ATTR_INFRARED in kwargs: if ATTR_INFRARED in kwargs:
@ -377,124 +516,44 @@ class LIFXLight(Light):
power_on = kwargs.get(ATTR_POWER, False) power_on = kwargs.get(ATTR_POWER, False)
power_off = not kwargs.get(ATTR_POWER, True) power_off = not kwargs.get(ATTR_POWER, True)
hsbk, changed_color = self.find_hsbk(**kwargs) hsbk = merge_hsbk(self.device.color, find_hsbk(**kwargs))
_LOGGER.debug("turn_on: %s (%d) %d %d %d %d %d",
self.who, self._power, fade, *hsbk)
if self._power == 0: # Send messages, waiting for ACK each time
ack = AwaitAioLIFX(self).wait
bulb = self.device
if not self.is_on:
if power_off: if power_off:
self.device.set_power(False, None, 0) yield from ack(partial(bulb.set_power, False))
if changed_color: if hsbk:
self.device.set_color(hsbk, None, 0) yield from ack(partial(bulb.set_color, hsbk))
if power_on: if power_on:
self.device.set_power(True, None, fade) yield from ack(partial(bulb.set_power, True, duration=fade))
else: else:
if power_on: if power_on:
self.device.set_power(True, None, 0) yield from ack(partial(bulb.set_power, True))
if changed_color: if hsbk:
self.device.set_color(hsbk, None, fade) yield from ack(partial(bulb.set_color, hsbk, duration=fade))
if power_off: if power_off:
self.device.set_power(False, None, fade) yield from ack(partial(bulb.set_power, False, duration=fade))
if power_on: # Schedule an update when the transition is complete
self.update_later(0) self.update_later(fade)
else:
self.update_later(fade)
if fade <= BULB_LATENCY: @asyncio.coroutine
if power_on: def default_effect(self, **kwargs):
self.set_power(1) """Start an effect with default parameters."""
if power_off: service = kwargs[ATTR_EFFECT]
self.set_power(0) data = {
if changed_color: ATTR_ENTITY_ID: self.entity_id,
self.set_color(*hsbk) }
yield from self.hass.services.async_call(DOMAIN, service, data)
@asyncio.coroutine @asyncio.coroutine
def async_update(self): def async_update(self):
"""Update bulb status (if it is available).""" """Update bulb status."""
_LOGGER.debug("%s async_update", self.who) _LOGGER.debug("%s async_update", self.who)
if self.blocker is None:
yield from self.refresh_state()
@asyncio.coroutine
def stop_effect(self):
"""Stop the currently running effect (if any)."""
if self.effect_data:
yield from self.effect_data.effect.async_restore(self)
@asyncio.coroutine
def refresh_state(self):
"""Ask the device about its current state and update our copy."""
if self.available: if self.available:
msg = yield from AwaitAioLIFX(self).wait(self.device.get_color) # Avoid state ping-pong by holding off updates as the state settles
if msg is not None: yield from asyncio.sleep(0.25)
self.set_power(self.device.power_level) yield from AwaitAioLIFX(self).wait(self.device.get_color)
self.set_color(*self.device.color)
self._name = self.device.label
def find_hsbk(self, **kwargs):
"""Find the desired color from a number of possible inputs."""
changed_color = False
hsbk = kwargs.pop(ATTR_HSBK, None)
if hsbk is not None:
return [hsbk, True]
preprocess_turn_on_alternatives(kwargs)
if ATTR_RGB_COLOR in kwargs:
hue, saturation, brightness = \
color_util.color_RGB_to_hsv(*kwargs[ATTR_RGB_COLOR])
saturation = convert_8_to_16(saturation)
brightness = convert_8_to_16(brightness)
changed_color = True
else:
hue = self._hue
saturation = self._sat
brightness = self._bri
if ATTR_XY_COLOR in kwargs:
hue, saturation = color_util.color_xy_to_hs(*kwargs[ATTR_XY_COLOR])
saturation = convert_8_to_16(saturation)
changed_color = True
# When color or temperature is set, use a default value for the other
if ATTR_COLOR_TEMP in kwargs:
kelvin = int(color_util.color_temperature_mired_to_kelvin(
kwargs[ATTR_COLOR_TEMP]))
if not changed_color:
saturation = 0
changed_color = True
else:
if changed_color:
kelvin = 3500
else:
kelvin = self._kel
if ATTR_BRIGHTNESS in kwargs:
brightness = convert_8_to_16(kwargs[ATTR_BRIGHTNESS])
changed_color = True
else:
brightness = self._bri
return [[hue, saturation, brightness, kelvin], changed_color]
def set_power(self, power):
"""Set power state value."""
_LOGGER.debug("set_power: %d", power)
self._power = (power != 0)
def set_color(self, hue, sat, bri, kel):
"""Set color state values."""
self._hue = hue
self._sat = sat
self._bri = bri
self._kel = kel
red, green, blue = color_util.color_hsv_to_RGB(
hue, convert_16_to_8(sat), convert_16_to_8(bri))
_LOGGER.debug("set_color: %d %d %d %d [%d %d %d]",
hue, sat, bri, kel, red, green, blue)
self._rgb = [red, green, blue]

View File

@ -1,388 +0,0 @@
"""Support for light effects for the LIFX light platform."""
import logging
import asyncio
import random
import voluptuous as vol
from homeassistant.components.light import (
DOMAIN, ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, ATTR_COLOR_NAME,
ATTR_RGB_COLOR, ATTR_COLOR_TEMP, ATTR_KELVIN, ATTR_EFFECT, ATTR_TRANSITION,
VALID_BRIGHTNESS, VALID_BRIGHTNESS_PCT)
from homeassistant.const import (ATTR_ENTITY_ID)
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
SERVICE_EFFECT_BREATHE = 'lifx_effect_breathe'
SERVICE_EFFECT_PULSE = 'lifx_effect_pulse'
SERVICE_EFFECT_COLORLOOP = 'lifx_effect_colorloop'
SERVICE_EFFECT_STOP = 'lifx_effect_stop'
ATTR_POWER_ON = 'power_on'
ATTR_PERIOD = 'period'
ATTR_CYCLES = 'cycles'
ATTR_MODE = 'mode'
ATTR_SPREAD = 'spread'
ATTR_CHANGE = 'change'
MODE_BLINK = 'blink'
MODE_BREATHE = 'breathe'
MODE_PING = 'ping'
MODE_STROBE = 'strobe'
MODE_SOLID = 'solid'
MODES = [MODE_BLINK, MODE_BREATHE, MODE_PING, MODE_STROBE, MODE_SOLID]
# aiolifx waveform modes
WAVEFORM_SINE = 1
WAVEFORM_PULSE = 4
NEUTRAL_WHITE = 3500
LIFX_EFFECT_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
vol.Optional(ATTR_POWER_ON, default=True): cv.boolean,
})
LIFX_EFFECT_BREATHE_SCHEMA = LIFX_EFFECT_SCHEMA.extend({
ATTR_BRIGHTNESS: VALID_BRIGHTNESS,
ATTR_BRIGHTNESS_PCT: VALID_BRIGHTNESS_PCT,
ATTR_COLOR_NAME: cv.string,
ATTR_RGB_COLOR: vol.All(vol.ExactSequence((cv.byte, cv.byte, cv.byte)),
vol.Coerce(tuple)),
ATTR_COLOR_TEMP: vol.All(vol.Coerce(int), vol.Range(min=1)),
ATTR_KELVIN: vol.All(vol.Coerce(int), vol.Range(min=0)),
ATTR_PERIOD: vol.All(vol.Coerce(float), vol.Range(min=0.05)),
ATTR_CYCLES: vol.All(vol.Coerce(float), vol.Range(min=1)),
})
LIFX_EFFECT_PULSE_SCHEMA = LIFX_EFFECT_BREATHE_SCHEMA.extend({
vol.Optional(ATTR_MODE, default=MODE_BLINK): vol.In(MODES),
})
LIFX_EFFECT_COLORLOOP_SCHEMA = LIFX_EFFECT_SCHEMA.extend({
ATTR_BRIGHTNESS: VALID_BRIGHTNESS,
ATTR_BRIGHTNESS_PCT: VALID_BRIGHTNESS_PCT,
vol.Optional(ATTR_PERIOD, default=60):
vol.All(vol.Coerce(float), vol.Clamp(min=0.05)),
vol.Optional(ATTR_CHANGE, default=20):
vol.All(vol.Coerce(float), vol.Clamp(min=0, max=360)),
vol.Optional(ATTR_SPREAD, default=30):
vol.All(vol.Coerce(float), vol.Clamp(min=0, max=360)),
ATTR_TRANSITION: vol.All(vol.Coerce(float), vol.Range(min=0)),
})
LIFX_EFFECT_STOP_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
vol.Optional(ATTR_POWER_ON, default=False): cv.boolean,
})
def setup(hass, lifx_manager):
"""Register the LIFX effects as hass service calls."""
@asyncio.coroutine
def async_service_handle(service):
"""Apply a service."""
entities = lifx_manager.service_to_entities(service)
if entities:
yield from start_effect(hass, entities,
service.service, **service.data)
descriptions = lifx_manager.get_descriptions()
hass.services.async_register(
DOMAIN, SERVICE_EFFECT_BREATHE, async_service_handle,
descriptions.get(SERVICE_EFFECT_BREATHE),
schema=LIFX_EFFECT_BREATHE_SCHEMA)
hass.services.async_register(
DOMAIN, SERVICE_EFFECT_PULSE, async_service_handle,
descriptions.get(SERVICE_EFFECT_PULSE),
schema=LIFX_EFFECT_PULSE_SCHEMA)
hass.services.async_register(
DOMAIN, SERVICE_EFFECT_COLORLOOP, async_service_handle,
descriptions.get(SERVICE_EFFECT_COLORLOOP),
schema=LIFX_EFFECT_COLORLOOP_SCHEMA)
hass.services.async_register(
DOMAIN, SERVICE_EFFECT_STOP, async_service_handle,
descriptions.get(SERVICE_EFFECT_STOP),
schema=LIFX_EFFECT_STOP_SCHEMA)
@asyncio.coroutine
def start_effect(hass, devices, service, **data):
"""Start a light effect."""
tasks = []
for light in devices:
tasks.append(hass.async_add_job(light.stop_effect()))
yield from asyncio.wait(tasks, loop=hass.loop)
if service in SERVICE_EFFECT_BREATHE:
effect = LIFXEffectBreathe(hass, devices)
elif service in SERVICE_EFFECT_PULSE:
effect = LIFXEffectPulse(hass, devices)
elif service == SERVICE_EFFECT_COLORLOOP:
effect = LIFXEffectColorloop(hass, devices)
elif service == SERVICE_EFFECT_STOP:
effect = LIFXEffectStop(hass, devices)
hass.async_add_job(effect.async_perform(**data))
@asyncio.coroutine
def default_effect(light, **kwargs):
"""Start an effect with default parameters."""
service = kwargs[ATTR_EFFECT]
data = {
ATTR_ENTITY_ID: light.entity_id,
}
yield from light.hass.services.async_call(DOMAIN, service, data)
def effect_list(light):
"""Return the list of supported effects for this light."""
if light.lifxwhite:
return [
SERVICE_EFFECT_BREATHE,
SERVICE_EFFECT_PULSE,
SERVICE_EFFECT_STOP,
]
else:
return [
SERVICE_EFFECT_COLORLOOP,
SERVICE_EFFECT_BREATHE,
SERVICE_EFFECT_PULSE,
SERVICE_EFFECT_STOP,
]
class LIFXEffectData(object):
"""Structure describing a running effect."""
def __init__(self, effect, power, color):
"""Initialize data structure."""
self.effect = effect
self.power = power
self.color = color
class LIFXEffect(object):
"""Representation of a light effect running on a number of lights."""
def __init__(self, hass, lights):
"""Initialize the effect."""
self.hass = hass
self.lights = lights
@asyncio.coroutine
def async_perform(self, **kwargs):
"""Do common setup and play the effect."""
yield from self.async_setup(**kwargs)
yield from self.async_play(**kwargs)
@asyncio.coroutine
def async_setup(self, **kwargs):
"""Prepare all lights for the effect."""
for light in self.lights:
# Remember the current state (as far as we know it)
yield from light.refresh_state()
light.effect_data = LIFXEffectData(
self, light.is_on, light.device.color)
# Temporarily turn on power for the effect to be visible
if kwargs[ATTR_POWER_ON] and not light.is_on:
hsbk = self.from_poweroff_hsbk(light, **kwargs)
light.device.set_color(hsbk)
light.device.set_power(True)
# pylint: disable=no-self-use
@asyncio.coroutine
def async_play(self, **kwargs):
"""Play the effect."""
yield None
@asyncio.coroutine
def async_restore(self, light):
"""Restore to the original state (if we are still running)."""
if light in self.lights:
self.lights.remove(light)
if light.effect_data and light.effect_data.effect == self:
if not light.effect_data.power:
light.device.set_power(False)
yield from asyncio.sleep(0.5)
light.device.set_color(light.effect_data.color)
yield from asyncio.sleep(0.5)
light.effect_data = None
yield from light.refresh_state()
def from_poweroff_hsbk(self, light, **kwargs):
"""Return the color when starting from a powered off state."""
return [random.randint(0, 65535), 65535, 0, NEUTRAL_WHITE]
class LIFXEffectPulse(LIFXEffect):
"""Representation of a pulse effect."""
def __init__(self, hass, lights):
"""Initialize the pulse effect."""
super().__init__(hass, lights)
self.name = SERVICE_EFFECT_PULSE
@asyncio.coroutine
def async_play(self, **kwargs):
"""Play the effect on all lights."""
for light in self.lights:
self.hass.async_add_job(self.async_light_play(light, **kwargs))
@asyncio.coroutine
def async_light_play(self, light, **kwargs):
"""Play a light effect on the bulb."""
hsbk, color_changed = light.find_hsbk(**kwargs)
if kwargs[ATTR_MODE] == MODE_STROBE:
# Strobe must flash from a dark color
light.device.set_color([0, 0, 0, NEUTRAL_WHITE])
yield from asyncio.sleep(0.1)
default_period = 0.1
default_cycles = 10
else:
default_period = 1.0
default_cycles = 1
period = kwargs.get(ATTR_PERIOD, default_period)
cycles = kwargs.get(ATTR_CYCLES, default_cycles)
# Breathe has a special waveform
if kwargs[ATTR_MODE] == MODE_BREATHE:
waveform = WAVEFORM_SINE
else:
waveform = WAVEFORM_PULSE
# Ping and solid have special duty cycles
if kwargs[ATTR_MODE] == MODE_PING:
ping_duration = int(5000 - min(2500, 300*period))
duty_cycle = 2**15 - ping_duration
elif kwargs[ATTR_MODE] == MODE_SOLID:
duty_cycle = -2**15
else:
duty_cycle = 0
# Set default effect color based on current setting
if not color_changed:
if kwargs[ATTR_MODE] == MODE_STROBE:
# Strobe: cold white
hsbk = [hsbk[0], 0, 65535, 5600]
elif light.lifxwhite or hsbk[1] < 65536/2:
# White: toggle brightness
hsbk[2] = 65535 if hsbk[2] < 65536/2 else 0
else:
# Color: fully desaturate with full brightness
hsbk = [hsbk[0], 0, 65535, 4000]
# Start the effect
args = {
'transient': 1,
'color': hsbk,
'period': int(period*1000),
'cycles': cycles,
'duty_cycle': duty_cycle,
'waveform': waveform,
}
light.device.set_waveform(args)
# Wait for completion and restore the initial state
yield from asyncio.sleep(period*cycles)
yield from self.async_restore(light)
def from_poweroff_hsbk(self, light, **kwargs):
"""Return the color is the target color, but no brightness."""
hsbk, _ = light.find_hsbk(**kwargs)
return [hsbk[0], hsbk[1], 0, hsbk[2]]
class LIFXEffectBreathe(LIFXEffectPulse):
"""Representation of a breathe effect."""
def __init__(self, hass, lights):
"""Initialize the breathe effect."""
super().__init__(hass, lights)
self.name = SERVICE_EFFECT_BREATHE
_LOGGER.warning("'lifx_effect_breathe' is deprecated. Please use "
"'lifx_effect_pulse' with 'mode: breathe'")
@asyncio.coroutine
def async_perform(self, **kwargs):
"""Prepare all lights for the effect."""
kwargs[ATTR_MODE] = MODE_BREATHE
yield from super().async_perform(**kwargs)
class LIFXEffectColorloop(LIFXEffect):
"""Representation of a colorloop effect."""
def __init__(self, hass, lights):
"""Initialize the colorloop effect."""
super().__init__(hass, lights)
self.name = SERVICE_EFFECT_COLORLOOP
@asyncio.coroutine
def async_play(self, **kwargs):
"""Play the effect on all lights."""
period = kwargs[ATTR_PERIOD]
spread = kwargs[ATTR_SPREAD]
change = kwargs[ATTR_CHANGE]
direction = 1 if random.randint(0, 1) else -1
# Random start
hue = random.uniform(0, 360) % 360
while self.lights:
hue = (hue + direction*change) % 360
random.shuffle(self.lights)
lhue = hue
for light in self.lights:
if ATTR_TRANSITION in kwargs:
transition = int(1000*kwargs[ATTR_TRANSITION])
elif light == self.lights[0] or spread > 0:
transition = int(1000 * random.uniform(period/2, period))
if ATTR_BRIGHTNESS in kwargs:
brightness = int(65535/255*kwargs[ATTR_BRIGHTNESS])
else:
brightness = light.effect_data.color[2]
hsbk = [
int(65535/360*lhue),
int(random.uniform(0.8, 1.0)*65535),
brightness,
NEUTRAL_WHITE,
]
light.device.set_color(hsbk, None, transition)
# Adjust the next light so the full spread is used
if len(self.lights) > 1:
lhue = (lhue + spread/(len(self.lights)-1)) % 360
yield from asyncio.sleep(period)
class LIFXEffectStop(LIFXEffect):
"""A no-op effect, but starting it will stop an existing effect."""
def __init__(self, hass, lights):
"""Initialize the stop effect."""
super().__init__(hass, lights)
self.name = SERVICE_EFFECT_STOP
@asyncio.coroutine
def async_perform(self, **kwargs):
"""Do nothing."""
yield None

View File

@ -1,98 +0,0 @@
lifx_set_state:
description: Set a color/brightness and possibliy turn the light on/off
fields:
entity_id:
description: Name(s) of entities to set a state on
example: 'light.garage'
'...':
description: All turn_on parameters can be used to specify a color
infrared:
description: Automatic infrared level (0..255) when light brightness is low
example: 255
transition:
description: Duration in seconds it takes to get to the final state
example: 10
power:
description: Turn the light on (True) or off (False). Leave out to keep the power as it is.
example: True
lifx_effect_breathe:
description: Deprecated, use lifx_effect_pulse
lifx_effect_pulse:
description: Run a flash effect by changing to a color and back.
fields:
entity_id:
description: Name(s) of entities to run the effect on
example: 'light.kitchen'
mode:
description: 'Decides how colors are changed. Possible values: blink, breathe, ping, strobe, solid'
example: strobe
brightness:
description: Number between 0..255 indicating brightness of the temporary color
example: 120
color_name:
description: A human readable color name
example: 'red'
rgb_color:
description: The temporary color in RGB-format
example: '[255, 100, 100]'
period:
description: Duration of the effect in seconds (default 1.0)
example: 3
cycles:
description: Number of times the effect should run (default 1.0)
example: 2
power_on:
description: Powered off lights are temporarily turned on during the effect (default True)
example: False
lifx_effect_colorloop:
description: Run an effect with looping colors.
fields:
entity_id:
description: Name(s) of entities to run the effect on
example: 'light.disco1, light.disco2, light.disco3'
brightness:
description: Number between 0 and 255 indicating brightness of the effect. Leave this out to maintain the current brightness of each participating light
example: 120
period:
description: Duration (in seconds) between color changes (default 60)
example: 180
change:
description: Hue movement per period, in degrees on a color wheel (ranges from 0 to 360, default 20)
example: 45
spread:
description: Maximum hue difference between participating lights, in degrees on a color wheel (ranges from 0 to 360, default 30)
example: 0
power_on:
description: Powered off lights are temporarily turned on during the effect (default True)
example: False
lifx_effect_stop:
description: Stop a running effect.
fields:
entity_id:
description: Name(s) of entities to stop effects on. Leave out to stop effects everywhere.
example: 'light.bedroom'

View File

@ -24,11 +24,13 @@ CONF_BRIDGES = 'bridges'
CONF_GROUPS = 'groups' CONF_GROUPS = 'groups'
CONF_NUMBER = 'number' CONF_NUMBER = 'number'
CONF_VERSION = 'version' CONF_VERSION = 'version'
CONF_FADE = 'fade'
DEFAULT_LED_TYPE = 'rgbw' DEFAULT_LED_TYPE = 'rgbw'
DEFAULT_PORT = 5987 DEFAULT_PORT = 5987
DEFAULT_TRANSITION = 0 DEFAULT_TRANSITION = 0
DEFAULT_VERSION = 6 DEFAULT_VERSION = 6
DEFAULT_FADE = False
LED_TYPE = ['rgbw', 'rgbww', 'white', 'bridge-led'] LED_TYPE = ['rgbw', 'rgbww', 'white', 'bridge-led']
@ -58,6 +60,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_TYPE, default=DEFAULT_LED_TYPE): vol.Optional(CONF_TYPE, default=DEFAULT_LED_TYPE):
vol.In(LED_TYPE), vol.In(LED_TYPE),
vol.Required(CONF_NUMBER): cv.positive_int, vol.Required(CONF_NUMBER): cv.positive_int,
vol.Optional(CONF_FADE, default=DEFAULT_FADE): cv.boolean,
} }
]), ]),
}, },
@ -112,7 +115,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
group_conf.get(CONF_NUMBER), group_conf.get(CONF_NUMBER),
group_conf.get(CONF_NAME), group_conf.get(CONF_NAME),
group_conf.get(CONF_TYPE, DEFAULT_LED_TYPE)) group_conf.get(CONF_TYPE, DEFAULT_LED_TYPE))
lights.append(LimitlessLEDGroup.factory(group)) lights.append(LimitlessLEDGroup.factory(group, {
'fade': group_conf[CONF_FADE]
}))
add_devices(lights) add_devices(lights)
@ -152,25 +157,26 @@ def state(new_state):
class LimitlessLEDGroup(Light): class LimitlessLEDGroup(Light):
"""Representation of a LimitessLED group.""" """Representation of a LimitessLED group."""
def __init__(self, group): def __init__(self, group, config):
"""Initialize a group.""" """Initialize a group."""
self.group = group self.group = group
self.repeating = False self.repeating = False
self._is_on = False self._is_on = False
self._brightness = None self._brightness = None
self.config = config
@staticmethod @staticmethod
def factory(group): def factory(group, config):
"""Produce LimitlessLEDGroup objects.""" """Produce LimitlessLEDGroup objects."""
from limitlessled.group.rgbw import RgbwGroup from limitlessled.group.rgbw import RgbwGroup
from limitlessled.group.white import WhiteGroup from limitlessled.group.white import WhiteGroup
from limitlessled.group.rgbww import RgbwwGroup from limitlessled.group.rgbww import RgbwwGroup
if isinstance(group, WhiteGroup): if isinstance(group, WhiteGroup):
return LimitlessLEDWhiteGroup(group) return LimitlessLEDWhiteGroup(group, config)
elif isinstance(group, RgbwGroup): elif isinstance(group, RgbwGroup):
return LimitlessLEDRGBWGroup(group) return LimitlessLEDRGBWGroup(group, config)
elif isinstance(group, RgbwwGroup): elif isinstance(group, RgbwwGroup):
return LimitlessLEDRGBWWGroup(group) return LimitlessLEDRGBWWGroup(group, config)
@property @property
def should_poll(self): def should_poll(self):
@ -196,15 +202,17 @@ class LimitlessLEDGroup(Light):
def turn_off(self, transition_time, pipeline, **kwargs): def turn_off(self, transition_time, pipeline, **kwargs):
"""Turn off a group.""" """Turn off a group."""
if self.is_on: if self.is_on:
pipeline.transition(transition_time, brightness=0.0).off() if self.config[CONF_FADE]:
pipeline.transition(transition_time, brightness=0.0)
pipeline.off()
class LimitlessLEDWhiteGroup(LimitlessLEDGroup): class LimitlessLEDWhiteGroup(LimitlessLEDGroup):
"""Representation of a LimitlessLED White group.""" """Representation of a LimitlessLED White group."""
def __init__(self, group): def __init__(self, group, config):
"""Initialize White group.""" """Initialize White group."""
super().__init__(group) super().__init__(group, config)
# Initialize group with known values. # Initialize group with known values.
self.group.on = True self.group.on = True
self.group.temperature = 1.0 self.group.temperature = 1.0
@ -242,9 +250,9 @@ class LimitlessLEDWhiteGroup(LimitlessLEDGroup):
class LimitlessLEDRGBWGroup(LimitlessLEDGroup): class LimitlessLEDRGBWGroup(LimitlessLEDGroup):
"""Representation of a LimitlessLED RGBW group.""" """Representation of a LimitlessLED RGBW group."""
def __init__(self, group): def __init__(self, group, config):
"""Initialize RGBW group.""" """Initialize RGBW group."""
super().__init__(group) super().__init__(group, config)
# Initialize group with known values. # Initialize group with known values.
self.group.on = True self.group.on = True
self.group.white() self.group.white()
@ -301,9 +309,9 @@ class LimitlessLEDRGBWGroup(LimitlessLEDGroup):
class LimitlessLEDRGBWWGroup(LimitlessLEDGroup): class LimitlessLEDRGBWWGroup(LimitlessLEDGroup):
"""Representation of a LimitlessLED RGBWW group.""" """Representation of a LimitlessLED RGBWW group."""
def __init__(self, group): def __init__(self, group, config):
"""Initialize RGBWW group.""" """Initialize RGBWW group."""
super().__init__(group) super().__init__(group, config)
# Initialize group with known values. # Initialize group with known values.
self.group.on = True self.group.on = True
self.group.white() self.group.white()

View File

@ -101,3 +101,98 @@ hue_activate_scene:
scene_name: scene_name:
description: Name of hue scene from the hue app description: Name of hue scene from the hue app
example: "Energize" example: "Energize"
lifx_set_state:
description: Set a color/brightness and possibliy turn the light on/off
fields:
entity_id:
description: Name(s) of entities to set a state on
example: 'light.garage'
'...':
description: All turn_on parameters can be used to specify a color
infrared:
description: Automatic infrared level (0..255) when light brightness is low
example: 255
transition:
description: Duration in seconds it takes to get to the final state
example: 10
power:
description: Turn the light on (True) or off (False). Leave out to keep the power as it is.
example: True
lifx_effect_pulse:
description: Run a flash effect by changing to a color and back.
fields:
entity_id:
description: Name(s) of entities to run the effect on
example: 'light.kitchen'
mode:
description: 'Decides how colors are changed. Possible values: blink, breathe, ping, strobe, solid'
example: strobe
brightness:
description: Number between 0..255 indicating brightness of the temporary color
example: 120
color_name:
description: A human readable color name
example: 'red'
rgb_color:
description: The temporary color in RGB-format
example: '[255, 100, 100]'
period:
description: Duration of the effect in seconds (default 1.0)
example: 3
cycles:
description: Number of times the effect should run (default 1.0)
example: 2
power_on:
description: Powered off lights are temporarily turned on during the effect (default True)
example: False
lifx_effect_colorloop:
description: Run an effect with looping colors.
fields:
entity_id:
description: Name(s) of entities to run the effect on
example: 'light.disco1, light.disco2, light.disco3'
brightness:
description: Number between 0 and 255 indicating brightness of the effect. Leave this out to maintain the current brightness of each participating light
example: 120
period:
description: Duration (in seconds) between color changes (default 60)
example: 180
change:
description: Hue movement per period, in degrees on a color wheel (ranges from 0 to 360, default 20)
example: 45
spread:
description: Maximum hue difference between participating lights, in degrees on a color wheel (ranges from 0 to 360, default 30)
example: 0
power_on:
description: Powered off lights are temporarily turned on during the effect (default True)
example: False
lifx_effect_stop:
description: Stop a running effect.
fields:
entity_id:
description: Name(s) of entities to stop effects on. Leave out to stop effects everywhere.
example: 'light.bedroom'

View File

@ -5,7 +5,8 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/verisure/ https://home-assistant.io/components/verisure/
""" """
import logging import logging
from time import sleep
from time import time
from homeassistant.components.verisure import HUB as hub from homeassistant.components.verisure import HUB as hub
from homeassistant.components.verisure import (CONF_LOCKS, CONF_CODE_DIGITS) from homeassistant.components.verisure import (CONF_LOCKS, CONF_CODE_DIGITS)
from homeassistant.components.lock import LockDevice from homeassistant.components.lock import LockDevice
@ -19,28 +20,32 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Verisure platform.""" """Set up the Verisure platform."""
locks = [] locks = []
if int(hub.config.get(CONF_LOCKS, 1)): if int(hub.config.get(CONF_LOCKS, 1)):
hub.update_locks() hub.update_overview()
locks.extend([ locks.extend([
VerisureDoorlock(device_id) VerisureDoorlock(device_label)
for device_id in hub.lock_status for device_label in hub.get(
]) "$.doorLockStatusList[*].deviceLabel")])
add_devices(locks) add_devices(locks)
class VerisureDoorlock(LockDevice): class VerisureDoorlock(LockDevice):
"""Representation of a Verisure doorlock.""" """Representation of a Verisure doorlock."""
def __init__(self, device_id): def __init__(self, device_label):
"""Initialize the Verisure lock.""" """Initialize the Verisure lock."""
self._id = device_id self._device_label = device_label
self._state = STATE_UNKNOWN self._state = STATE_UNKNOWN
self._digits = hub.config.get(CONF_CODE_DIGITS) self._digits = hub.config.get(CONF_CODE_DIGITS)
self._changed_by = None self._changed_by = None
self._change_timestamp = 0
@property @property
def name(self): def name(self):
"""Return the name of the lock.""" """Return the name of the lock."""
return '{}'.format(hub.lock_status[self._id].location) return hub.get_first(
"$.doorLockStatusList[?(@.deviceLabel=='%s')].area",
self._device_label)
@property @property
def state(self): def state(self):
@ -50,7 +55,9 @@ class VerisureDoorlock(LockDevice):
@property @property
def available(self): def available(self):
"""Return True if entity is available.""" """Return True if entity is available."""
return hub.available return hub.get_first(
"$.doorLockStatusList[?(@.deviceLabel=='%s')]",
self._device_label) is not None
@property @property
def changed_by(self): def changed_by(self):
@ -64,32 +71,52 @@ class VerisureDoorlock(LockDevice):
def update(self): def update(self):
"""Update lock status.""" """Update lock status."""
hub.update_locks() if time() - self._change_timestamp < 10:
return
if hub.lock_status[self._id].status == 'unlocked': hub.update_overview()
status = hub.get_first(
"$.doorLockStatusList[?(@.deviceLabel=='%s')].lockedState",
self._device_label)
if status == 'UNLOCKED':
self._state = STATE_UNLOCKED self._state = STATE_UNLOCKED
elif hub.lock_status[self._id].status == 'locked': elif status == 'LOCKED':
self._state = STATE_LOCKED self._state = STATE_LOCKED
elif hub.lock_status[self._id].status != 'pending': elif status != 'PENDING':
_LOGGER.error( _LOGGER.error('Unknown lock state %s', status)
"Unknown lock state %s", hub.lock_status[self._id].status) self._changed_by = hub.get_first(
self._changed_by = hub.lock_status[self._id].name "$.doorLockStatusList[?(@.deviceLabel=='%s')].userString",
self._device_label)
@property @property
def is_locked(self): def is_locked(self):
"""Return true if lock is locked.""" """Return true if lock is locked."""
return hub.lock_status[self._id].status return self._state == STATE_LOCKED
def unlock(self, **kwargs): def unlock(self, **kwargs):
"""Send unlock command.""" """Send unlock command."""
hub.my_pages.lock.set(kwargs[ATTR_CODE], self._id, 'UNLOCKED') if self._state == STATE_UNLOCKED:
_LOGGER.debug("Verisure doorlock unlocking") return
hub.my_pages.lock.wait_while_pending() self.set_lock_state(kwargs[ATTR_CODE], STATE_UNLOCKED)
self.update()
def lock(self, **kwargs): def lock(self, **kwargs):
"""Send lock command.""" """Send lock command."""
hub.my_pages.lock.set(kwargs[ATTR_CODE], self._id, 'LOCKED') if self._state == STATE_LOCKED:
_LOGGER.debug("Verisure doorlock locking") return
hub.my_pages.lock.wait_while_pending() self.set_lock_state(kwargs[ATTR_CODE], STATE_LOCKED)
self.update()
def set_lock_state(self, code, state):
"""Send set lock state command."""
lock_state = 'lock' if state == STATE_LOCKED else 'unlock'
transaction_id = hub.session.set_lock_state(
code,
self._device_label,
lock_state)['doorLockStateChangeTransactionId']
_LOGGER.debug("Verisure doorlock %s", state)
transaction = {}
while 'result' not in transaction:
sleep(0.5)
transaction = hub.session.get_lock_state_transaction(
transaction_id)
if transaction['result'] == 'OK':
self._state = state
self._change_timestamp = time()

View File

@ -6,6 +6,7 @@ https://home-assistant.io/components/media_player.denon/
""" """
import logging import logging
from collections import namedtuple
import voluptuous as vol import voluptuous as vol
from homeassistant.components.media_player import ( from homeassistant.components.media_player import (
@ -16,16 +17,19 @@ from homeassistant.components.media_player import (
MEDIA_TYPE_MUSIC, SUPPORT_VOLUME_SET, SUPPORT_PLAY) MEDIA_TYPE_MUSIC, SUPPORT_VOLUME_SET, SUPPORT_PLAY)
from homeassistant.const import ( from homeassistant.const import (
CONF_HOST, STATE_OFF, STATE_PLAYING, STATE_PAUSED, CONF_HOST, STATE_OFF, STATE_PLAYING, STATE_PAUSED,
CONF_NAME, STATE_ON) CONF_NAME, STATE_ON, CONF_ZONE)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['denonavr==0.4.4'] REQUIREMENTS = ['denonavr==0.5.1']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = None DEFAULT_NAME = None
DEFAULT_SHOW_SOURCES = False DEFAULT_SHOW_SOURCES = False
CONF_SHOW_ALL_SOURCES = 'show_all_sources' CONF_SHOW_ALL_SOURCES = 'show_all_sources'
CONF_ZONES = 'zones'
CONF_VALID_ZONES = ['Zone2', 'Zone3']
CONF_INVALID_ZONES_ERR = 'Invalid Zone (expected Zone2 or Zone3)'
KEY_DENON_CACHE = 'denonavr_hosts' KEY_DENON_CACHE = 'denonavr_hosts'
SUPPORT_DENON = SUPPORT_VOLUME_STEP | SUPPORT_VOLUME_MUTE | \ SUPPORT_DENON = SUPPORT_VOLUME_STEP | SUPPORT_VOLUME_MUTE | \
@ -36,16 +40,26 @@ SUPPORT_MEDIA_MODES = SUPPORT_PLAY_MEDIA | \
SUPPORT_PAUSE | SUPPORT_PREVIOUS_TRACK | \ SUPPORT_PAUSE | SUPPORT_PREVIOUS_TRACK | \
SUPPORT_NEXT_TRACK | SUPPORT_VOLUME_SET | SUPPORT_PLAY SUPPORT_NEXT_TRACK | SUPPORT_VOLUME_SET | SUPPORT_PLAY
DENON_ZONE_SCHEMA = vol.Schema({
vol.Required(CONF_ZONE): vol.In(CONF_VALID_ZONES, CONF_INVALID_ZONES_ERR),
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
})
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_HOST): cv.string, vol.Optional(CONF_HOST): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_SHOW_ALL_SOURCES, default=DEFAULT_SHOW_SOURCES): vol.Optional(CONF_SHOW_ALL_SOURCES, default=DEFAULT_SHOW_SOURCES):
cv.boolean, cv.boolean,
vol.Optional(CONF_ZONES):
vol.All(cv.ensure_list, [DENON_ZONE_SCHEMA])
}) })
NewHost = namedtuple('NewHost', ['host', 'name'])
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Denon platform.""" """Set up the Denon platform."""
# pylint: disable=import-error
import denonavr import denonavr
# Initialize list with receivers to be started # Initialize list with receivers to be started
@ -55,28 +69,32 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
if cache is None: if cache is None:
cache = hass.data[KEY_DENON_CACHE] = set() cache = hass.data[KEY_DENON_CACHE] = set()
# Start assignment of host and name # Get config option for show_all_sources
show_all_sources = config.get(CONF_SHOW_ALL_SOURCES) show_all_sources = config.get(CONF_SHOW_ALL_SOURCES)
# Get config option for additional zones
zones = config.get(CONF_ZONES)
if zones is not None:
add_zones = {}
for entry in zones:
add_zones[entry[CONF_ZONE]] = entry[CONF_NAME]
else:
add_zones = None
# Start assignment of host and name
new_hosts = []
# 1. option: manual setting # 1. option: manual setting
if config.get(CONF_HOST) is not None: if config.get(CONF_HOST) is not None:
host = config.get(CONF_HOST) host = config.get(CONF_HOST)
name = config.get(CONF_NAME) name = config.get(CONF_NAME)
# Check if host not in cache, append it and save for later starting new_hosts.append(NewHost(host=host, name=name))
if host not in cache:
cache.add(host)
receivers.append(
DenonDevice(denonavr.DenonAVR(host, name, show_all_sources)))
_LOGGER.info("Denon receiver at host %s initialized", host)
# 2. option: discovery using netdisco # 2. option: discovery using netdisco
if discovery_info is not None: if discovery_info is not None:
host = discovery_info.get('host') host = discovery_info.get('host')
name = discovery_info.get('name') name = discovery_info.get('name')
# Check if host not in cache, append it and save for later starting new_hosts.append(NewHost(host=host, name=name))
if host not in cache:
cache.add(host)
receivers.append(
DenonDevice(denonavr.DenonAVR(host, name, show_all_sources)))
_LOGGER.info("Denon receiver at host %s initialized", host)
# 3. option: discovery using denonavr library # 3. option: discovery using denonavr library
if config.get(CONF_HOST) is None and discovery_info is None: if config.get(CONF_HOST) is None and discovery_info is None:
d_receivers = denonavr.discover() d_receivers = denonavr.discover()
@ -85,14 +103,18 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
for d_receiver in d_receivers: for d_receiver in d_receivers:
host = d_receiver["host"] host = d_receiver["host"]
name = d_receiver["friendlyName"] name = d_receiver["friendlyName"]
# Check if host not in cache, append it and save for later new_hosts.append(NewHost(host=host, name=name))
# starting
if host not in cache: for entry in new_hosts:
cache.add(host) # Check if host not in cache, append it and save for later
receivers.append( # starting
DenonDevice( if entry.host not in cache:
denonavr.DenonAVR(host, name, show_all_sources))) new_device = denonavr.DenonAVR(
_LOGGER.info("Denon receiver at host %s initialized", host) entry.host, entry.name, show_all_sources, add_zones)
for new_zone in new_device.zones.values():
receivers.append(DenonDevice(new_zone))
cache.add(host)
_LOGGER.info("Denon receiver at host %s initialized", host)
# Add all freshly discovered receivers # Add all freshly discovered receivers
if receivers: if receivers:

View File

@ -21,7 +21,7 @@ from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
REQUIREMENTS = ['pyemby==1.2'] REQUIREMENTS = ['pyemby==1.3']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -540,7 +540,7 @@ class KodiDevice(MediaPlayerDevice):
elif self._turn_off_action == 'shutdown': elif self._turn_off_action == 'shutdown':
yield from self.server.System.Shutdown() yield from self.server.System.Shutdown()
else: else:
_LOGGER.warning('turn_off requested but turn_off_action is none') _LOGGER.warning("turn_off requested but turn_off_action is none")
@cmd @cmd
@asyncio.coroutine @asyncio.coroutine
@ -694,22 +694,26 @@ class KodiDevice(MediaPlayerDevice):
def async_call_method(self, method, **kwargs): def async_call_method(self, method, **kwargs):
"""Run Kodi JSONRPC API method with params.""" """Run Kodi JSONRPC API method with params."""
import jsonrpc_base import jsonrpc_base
_LOGGER.debug('Run API method "%s", kwargs=%s', method, kwargs) _LOGGER.debug("Run API method %s, kwargs=%s", method, kwargs)
result_ok = False result_ok = False
try: try:
result = yield from getattr(self.server, method)(**kwargs) result = yield from getattr(self.server, method)(**kwargs)
result_ok = True result_ok = True
except jsonrpc_base.jsonrpc.ProtocolError as exc: except jsonrpc_base.jsonrpc.ProtocolError as exc:
result = exc.args[2]['error'] result = exc.args[2]['error']
_LOGGER.error('Run API method %s.%s(%s) error: %s', _LOGGER.error("Run API method %s.%s(%s) error: %s",
self.entity_id, method, kwargs, result) self.entity_id, method, kwargs, result)
except jsonrpc_base.jsonrpc.TransportError:
result = None
_LOGGER.warning("TransportError trying to run API method "
"%s.%s(%s)", self.entity_id, method, kwargs)
if isinstance(result, dict): if isinstance(result, dict):
event_data = {'entity_id': self.entity_id, event_data = {'entity_id': self.entity_id,
'result': result, 'result': result,
'result_ok': result_ok, 'result_ok': result_ok,
'input': {'method': method, 'params': kwargs}} 'input': {'method': method, 'params': kwargs}}
_LOGGER.debug('EVENT kodi_call_method_result: %s', event_data) _LOGGER.debug("EVENT kodi_call_method_result: %s", event_data)
self.hass.bus.async_fire(EVENT_KODI_CALL_METHOD_RESULT, self.hass.bus.async_fire(EVENT_KODI_CALL_METHOD_RESULT,
event_data=event_data) event_data=event_data)
return result return result
@ -753,10 +757,13 @@ class KodiDevice(MediaPlayerDevice):
yield from self.server.Playlist.Add(params) yield from self.server.Playlist.Add(params)
except jsonrpc_base.jsonrpc.ProtocolError as exc: except jsonrpc_base.jsonrpc.ProtocolError as exc:
result = exc.args[2]['error'] result = exc.args[2]['error']
_LOGGER.error('Run API method %s.Playlist.Add(%s) error: %s', _LOGGER.error("Run API method %s.Playlist.Add(%s) error: %s",
self.entity_id, media_type, result) self.entity_id, media_type, result)
except jsonrpc_base.jsonrpc.TransportError:
_LOGGER.warning("TransportError trying to add playlist to %s",
self.entity_id)
else: else:
_LOGGER.warning('No media detected for Playlist.Add') _LOGGER.warning("No media detected for Playlist.Add")
@asyncio.coroutine @asyncio.coroutine
def async_add_all_albums(self, artist_name): def async_add_all_albums(self, artist_name):
@ -800,7 +807,7 @@ class KodiDevice(MediaPlayerDevice):
artist_name, [a['artist'] for a in artists['artists']]) artist_name, [a['artist'] for a in artists['artists']])
return artists['artists'][out[0][0]]['artistid'] return artists['artists'][out[0][0]]['artistid']
except KeyError: except KeyError:
_LOGGER.warning('No artists were found: %s', artist_name) _LOGGER.warning("No artists were found: %s", artist_name)
return None return None
@asyncio.coroutine @asyncio.coroutine
@ -839,7 +846,7 @@ class KodiDevice(MediaPlayerDevice):
album_name, [a['label'] for a in albums['albums']]) album_name, [a['label'] for a in albums['albums']])
return albums['albums'][out[0][0]]['albumid'] return albums['albums'][out[0][0]]['albumid']
except KeyError: except KeyError:
_LOGGER.warning('No albums were found with artist: %s, album: %s', _LOGGER.warning("No albums were found with artist: %s, album: %s",
artist_name, album_name) artist_name, album_name)
return None return None

View File

@ -14,7 +14,8 @@ from homeassistant.components.media_player import (
MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, PLATFORM_SCHEMA, MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, PLATFORM_SCHEMA,
SUPPORT_PREVIOUS_TRACK, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_PREVIOUS_TRACK, SUPPORT_TURN_OFF, SUPPORT_TURN_ON,
SUPPORT_VOLUME_SET, SUPPORT_PLAY_MEDIA, SUPPORT_PLAY, MEDIA_TYPE_PLAYLIST, SUPPORT_VOLUME_SET, SUPPORT_PLAY_MEDIA, SUPPORT_PLAY, MEDIA_TYPE_PLAYLIST,
SUPPORT_SELECT_SOURCE, MediaPlayerDevice) SUPPORT_SELECT_SOURCE, SUPPORT_CLEAR_PLAYLIST, SUPPORT_SHUFFLE_SET,
SUPPORT_SEEK, MediaPlayerDevice)
from homeassistant.const import ( from homeassistant.const import (
STATE_OFF, STATE_PAUSED, STATE_PLAYING, CONF_PORT, CONF_PASSWORD, STATE_OFF, STATE_PAUSED, STATE_PLAYING, CONF_PORT, CONF_PASSWORD,
CONF_HOST, CONF_NAME) CONF_HOST, CONF_NAME)
@ -32,7 +33,8 @@ PLAYLIST_UPDATE_INTERVAL = timedelta(seconds=120)
SUPPORT_MPD = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_TURN_OFF | \ SUPPORT_MPD = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_TURN_OFF | \
SUPPORT_TURN_ON | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | \ SUPPORT_TURN_ON | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | \
SUPPORT_PLAY_MEDIA | SUPPORT_PLAY | SUPPORT_SELECT_SOURCE SUPPORT_PLAY_MEDIA | SUPPORT_PLAY | SUPPORT_SELECT_SOURCE | \
SUPPORT_CLEAR_PLAYLIST | SUPPORT_SHUFFLE_SET | SUPPORT_SEEK
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string, vol.Required(CONF_HOST): cv.string,
@ -266,3 +268,20 @@ class MpdDevice(MediaPlayerDevice):
self.client.clear() self.client.clear()
self.client.add(media_id) self.client.add(media_id)
self.client.play() self.client.play()
@property
def shuffle(self):
"""Boolean if shuffle is enabled."""
return bool(self.status['random'])
def set_shuffle(self, shuffle):
"""Enable/disable shuffle mode."""
self.client.random(int(shuffle))
def clear_playlist(self):
"""Clear players playlist."""
self.client.clear()
def media_seek(self, position):
"""Send seek command."""
self.client.seekcur(position)

View File

@ -88,6 +88,8 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
elif discovery_info is not None: elif discovery_info is not None:
# Parse discovery data # Parse discovery data
host = discovery_info.get('host') host = discovery_info.get('host')
port = discovery_info.get('port')
host = '%s:%s' % (host, port)
_LOGGER.info("Discovered PLEX server: %s", host) _LOGGER.info("Discovered PLEX server: %s", host)
if host in _CONFIGURING: if host in _CONFIGURING:
@ -106,6 +108,7 @@ def setup_plexserver(host, token, hass, config, add_devices_callback):
try: try:
plexserver = plexapi.server.PlexServer('http://%s' % host, token) plexserver = plexapi.server.PlexServer('http://%s' % host, token)
_LOGGER.info("Discovery configuration done (no token needed)")
except (plexapi.exceptions.BadRequest, plexapi.exceptions.Unauthorized, except (plexapi.exceptions.BadRequest, plexapi.exceptions.Unauthorized,
plexapi.exceptions.NotFound) as error: plexapi.exceptions.NotFound) as error:
_LOGGER.info(error) _LOGGER.info(error)

View File

@ -20,7 +20,7 @@ from homeassistant.const import (CONF_HOST, CONF_NAME, STATE_OFF, CONF_PORT,
STATE_PAUSED, STATE_PLAYING, STATE_PAUSED, STATE_PLAYING,
STATE_UNAVAILABLE) STATE_UNAVAILABLE)
REQUIREMENTS = ['libsoundtouch==0.3.0'] REQUIREMENTS = ['libsoundtouch==0.6.2']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -29,7 +29,7 @@ from homeassistant.const import (
CONF_PASSWORD, CONF_PORT, CONF_PROTOCOL, CONF_PAYLOAD) CONF_PASSWORD, CONF_PORT, CONF_PROTOCOL, CONF_PAYLOAD)
from homeassistant.components.mqtt.server import HBMQTT_CONFIG_SCHEMA from homeassistant.components.mqtt.server import HBMQTT_CONFIG_SCHEMA
REQUIREMENTS = ['paho-mqtt==1.2.3'] REQUIREMENTS = ['paho-mqtt==1.3.0']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -17,8 +17,8 @@ from homeassistant.components.mqtt import CONF_STATE_TOPIC
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
TOPIC_MATCHER = re.compile( TOPIC_MATCHER = re.compile(
r'(?P<prefix_topic>\w+)/(?P<component>\w+)/(?P<object_id>[a-zA-Z0-9_-]+)' r'(?P<prefix_topic>\w+)/(?P<component>\w+)/'
'/config') r'(?:(?P<node_id>[a-zA-Z0-9_-]+)/)?(?P<object_id>[a-zA-Z0-9_-]+)/config')
SUPPORTED_COMPONENTS = ['binary_sensor', 'light', 'sensor', 'switch'] SUPPORTED_COMPONENTS = ['binary_sensor', 'light', 'sensor', 'switch']
@ -44,7 +44,7 @@ def async_start(hass, discovery_topic, hass_config):
if not match: if not match:
return return
prefix_topic, component, object_id = match.groups() prefix_topic, component, node_id, object_id = match.groups()
try: try:
payload = json.loads(payload) payload = json.loads(payload)
@ -65,21 +65,25 @@ def async_start(hass, discovery_topic, hass_config):
payload[CONF_PLATFORM] = platform payload[CONF_PLATFORM] = platform
if CONF_STATE_TOPIC not in payload: if CONF_STATE_TOPIC not in payload:
payload[CONF_STATE_TOPIC] = '{}/{}/{}/state'.format( payload[CONF_STATE_TOPIC] = '{}/{}/{}{}/state'.format(
discovery_topic, component, object_id) discovery_topic, component, '%s/' % node_id if node_id else '',
object_id)
if ALREADY_DISCOVERED not in hass.data: if ALREADY_DISCOVERED not in hass.data:
hass.data[ALREADY_DISCOVERED] = set() hass.data[ALREADY_DISCOVERED] = set()
discovery_hash = (component, object_id) # If present, the node_id will be included in the discovered object id
discovery_id = '_'.join((node_id, object_id)) if node_id else object_id
discovery_hash = (component, discovery_id)
if discovery_hash in hass.data[ALREADY_DISCOVERED]: if discovery_hash in hass.data[ALREADY_DISCOVERED]:
_LOGGER.info("Component has already been discovered: %s %s", _LOGGER.info("Component has already been discovered: %s %s",
component, object_id) component, discovery_id)
return return
hass.data[ALREADY_DISCOVERED].add(discovery_hash) hass.data[ALREADY_DISCOVERED].add(discovery_hash)
_LOGGER.info("Found new component: %s %s", component, object_id) _LOGGER.info("Found new component: %s %s", component, discovery_id)
yield from async_load_platform( yield from async_load_platform(
hass, component, platform, payload, hass_config) hass, component, platform, payload, hass_config)

View File

@ -0,0 +1,80 @@
"""
Clicksend platform for notify component.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/notify.clicksend/
"""
import json
import logging
import requests
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.const import (
CONF_USERNAME, CONF_API_KEY, CONF_RECIPIENT, HTTP_HEADER_CONTENT_TYPE,
CONTENT_TYPE_JSON)
from homeassistant.components.notify import (
PLATFORM_SCHEMA, BaseNotificationService)
_LOGGER = logging.getLogger(__name__)
BASE_API_URL = 'https://rest.clicksend.com/v3'
HEADERS = {HTTP_HEADER_CONTENT_TYPE: CONTENT_TYPE_JSON}
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_API_KEY): cv.string,
vol.Required(CONF_RECIPIENT): cv.string,
})
def get_service(hass, config, discovery_info=None):
"""Get the ClickSend notification service."""
if _authenticate(config) is False:
_LOGGER.exception("You are not authorized to access ClickSend")
return None
return ClicksendNotificationService(config)
class ClicksendNotificationService(BaseNotificationService):
"""Implementation of a notification service for the ClickSend service."""
def __init__(self, config):
"""Initialize the service."""
self.username = config.get(CONF_USERNAME)
self.api_key = config.get(CONF_API_KEY)
self.recipient = config.get(CONF_RECIPIENT)
def send_message(self, message="", **kwargs):
"""Send a message to a user."""
data = ({'messages': [{'source': 'hass.notify', 'from': self.recipient,
'to': self.recipient, 'body': message}]})
api_url = "{}/sms/send".format(BASE_API_URL)
resp = requests.post(api_url, data=json.dumps(data), headers=HEADERS,
auth=(self.username, self.api_key), timeout=5)
obj = json.loads(resp.text)
response_msg = obj['response_msg']
response_code = obj['response_code']
if resp.status_code != 200:
_LOGGER.error("Error %s : %s (Code %s)", resp.status_code,
response_msg, response_code)
def _authenticate(config):
"""Authenticate with ClickSend."""
api_url = '{}/account'.format(BASE_API_URL)
resp = requests.get(api_url, headers=HEADERS,
auth=(config.get(CONF_USERNAME),
config.get(CONF_API_KEY)), timeout=5)
if resp.status_code != 200:
return False
return True

View File

@ -25,7 +25,7 @@ from homeassistant.components.http import HomeAssistantView
from homeassistant.components.frontend import add_manifest_json_key from homeassistant.components.frontend import add_manifest_json_key
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
REQUIREMENTS = ['pywebpush==1.0.4', 'PyJWT==1.5.0'] REQUIREMENTS = ['pywebpush==1.0.5', 'PyJWT==1.5.0']
DEPENDENCIES = ['frontend'] DEPENDENCIES = ['frontend']

View File

@ -29,16 +29,18 @@ _LOGGER = logging.getLogger(__name__)
ATTR_IMAGES = 'images' # optional embedded image file attachments ATTR_IMAGES = 'images' # optional embedded image file attachments
ATTR_HTML = 'html' ATTR_HTML = 'html'
CONF_STARTTLS = 'starttls' CONF_ENCRYPTION = 'encryption'
CONF_DEBUG = 'debug' CONF_DEBUG = 'debug'
CONF_SERVER = 'server' CONF_SERVER = 'server'
CONF_SENDER_NAME = 'sender_name' CONF_SENDER_NAME = 'sender_name'
DEFAULT_HOST = 'localhost' DEFAULT_HOST = 'localhost'
DEFAULT_PORT = 25 DEFAULT_PORT = 587
DEFAULT_TIMEOUT = 5 DEFAULT_TIMEOUT = 5
DEFAULT_DEBUG = False DEFAULT_DEBUG = False
DEFAULT_STARTTLS = False DEFAULT_ENCRYPTION = 'starttls'
ENCRYPTION_OPTIONS = ['tls', 'starttls', 'none']
# pylint: disable=no-value-for-parameter # pylint: disable=no-value-for-parameter
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
@ -47,7 +49,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_SERVER, default=DEFAULT_HOST): cv.string, vol.Optional(CONF_SERVER, default=DEFAULT_HOST): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
vol.Optional(CONF_STARTTLS, default=DEFAULT_STARTTLS): cv.boolean, vol.Optional(CONF_ENCRYPTION, default=DEFAULT_ENCRYPTION):
vol.In(ENCRYPTION_OPTIONS),
vol.Optional(CONF_USERNAME): cv.string, vol.Optional(CONF_USERNAME): cv.string,
vol.Optional(CONF_PASSWORD): cv.string, vol.Optional(CONF_PASSWORD): cv.string,
vol.Optional(CONF_SENDER_NAME): cv.string, vol.Optional(CONF_SENDER_NAME): cv.string,
@ -62,7 +65,7 @@ def get_service(hass, config, discovery_info=None):
config.get(CONF_PORT), config.get(CONF_PORT),
config.get(CONF_TIMEOUT), config.get(CONF_TIMEOUT),
config.get(CONF_SENDER), config.get(CONF_SENDER),
config.get(CONF_STARTTLS), config.get(CONF_ENCRYPTION),
config.get(CONF_USERNAME), config.get(CONF_USERNAME),
config.get(CONF_PASSWORD), config.get(CONF_PASSWORD),
config.get(CONF_RECIPIENT), config.get(CONF_RECIPIENT),
@ -78,28 +81,32 @@ def get_service(hass, config, discovery_info=None):
class MailNotificationService(BaseNotificationService): class MailNotificationService(BaseNotificationService):
"""Implement the notification service for E-mail messages.""" """Implement the notification service for E-mail messages."""
def __init__(self, server, port, timeout, sender, starttls, username, def __init__(self, server, port, timeout, sender, encryption, username,
password, recipients, sender_name, debug): password, recipients, sender_name, debug):
"""Initialize the SMTP service.""" """Initialize the SMTP service."""
self._server = server self._server = server
self._port = port self._port = port
self._timeout = timeout self._timeout = timeout
self._sender = sender self._sender = sender
self.starttls = starttls self.encryption = encryption
self.username = username self.username = username
self.password = password self.password = password
self.recipients = recipients self.recipients = recipients
self._sender_name = sender_name self._sender_name = sender_name
self._timeout = timeout
self.debug = debug self.debug = debug
self.tries = 2 self.tries = 2
def connect(self): def connect(self):
"""Connect/authenticate to SMTP Server.""" """Connect/authenticate to SMTP Server."""
mail = smtplib.SMTP(self._server, self._port, timeout=self._timeout) if self.encryption == "tls":
mail = smtplib.SMTP_SSL(
self._server, self._port, timeout=self._timeout)
else:
mail = smtplib.SMTP(
self._server, self._port, timeout=self._timeout)
mail.set_debuglevel(self.debug) mail.set_debuglevel(self.debug)
mail.ehlo_or_helo_if_needed() mail.ehlo_or_helo_if_needed()
if self.starttls: if self.encryption == "starttls":
mail.starttls() mail.starttls()
mail.ehlo() mail.ehlo()
if self.username and self.password: if self.username and self.password:

View File

@ -9,7 +9,7 @@ from homeassistant.exceptions import HomeAssistantError
from homeassistant.util import sanitize_filename from homeassistant.util import sanitize_filename
DOMAIN = 'python_script' DOMAIN = 'python_script'
REQUIREMENTS = ['restrictedpython==4.0a2'] REQUIREMENTS = ['restrictedpython==4.0a3']
FOLDER = 'python_scripts' FOLDER = 'python_scripts'
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -33,7 +33,7 @@ from . import purge, migration
from .const import DATA_INSTANCE from .const import DATA_INSTANCE
from .util import session_scope from .util import session_scope
REQUIREMENTS = ['sqlalchemy==1.1.10'] REQUIREMENTS = ['sqlalchemy==1.1.11']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -64,6 +64,8 @@ def _apply_update(engine, new_version):
# Create indexes for states # Create indexes for states
_create_index(engine, "states", "ix_states_last_updated") _create_index(engine, "states", "ix_states_last_updated")
_create_index(engine, "states", "ix_states_entity_id_created") _create_index(engine, "states", "ix_states_entity_id_created")
elif new_version == 3:
_create_index(engine, "states", "ix_states_created_domain")
else: else:
raise ValueError("No schema migration defined for version {}" raise ValueError("No schema migration defined for version {}"
.format(new_version)) .format(new_version))

View File

@ -16,7 +16,7 @@ from homeassistant.remote import JSONEncoder
# pylint: disable=invalid-name # pylint: disable=invalid-name
Base = declarative_base() Base = declarative_base()
SCHEMA_VERSION = 2 SCHEMA_VERSION = 3
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -75,7 +75,9 @@ class States(Base): # type: ignore
Index('states__significant_changes', Index('states__significant_changes',
'domain', 'last_updated', 'entity_id'), 'domain', 'last_updated', 'entity_id'),
Index('ix_states_entity_id_created', Index('ix_states_entity_id_created',
'entity_id', 'created'),) 'entity_id', 'created'),
Index('ix_states_created_domain',
'created', 'domain'),)
@staticmethod @staticmethod
def from_event(event): def from_event(event):

View File

@ -26,12 +26,13 @@ _LOGGER = logging.getLogger(__name__)
DEFAULT_PORT = 5222 DEFAULT_PORT = 5222
DEVICES = [] DEVICES = []
CONF_DEVICE_CACHE = 'device_cache'
SERVICE_SYNC = 'harmony_sync' SERVICE_SYNC = 'harmony_sync'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_NAME): cv.string, vol.Required(CONF_NAME): cv.string,
vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_HOST): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Required(ATTR_ACTIVITY, default=None): cv.string, vol.Required(ATTR_ACTIVITY, default=None): cv.string,
}) })
@ -44,29 +45,65 @@ HARMONY_SYNC_SCHEMA = vol.Schema({
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Harmony platform.""" """Set up the Harmony platform."""
import pyharmony import pyharmony
global DEVICES
name = config.get(CONF_NAME) host = None
host = config.get(CONF_HOST) activity = None
port = config.get(CONF_PORT)
_LOGGER.debug("Loading Harmony platform: %s", name)
harmony_conf_file = hass.config.path( if CONF_DEVICE_CACHE not in hass.data:
'{}{}{}'.format('harmony_', slugify(name), '.conf')) hass.data[CONF_DEVICE_CACHE] = []
if discovery_info:
# Find the discovered device in the list of user configurations
override = next((c for c in hass.data[CONF_DEVICE_CACHE]
if c.get(CONF_NAME) == discovery_info.get(CONF_NAME)),
False)
port = DEFAULT_PORT
if override:
activity = override.get(ATTR_ACTIVITY)
port = override.get(CONF_PORT, DEFAULT_PORT)
host = (
discovery_info.get(CONF_NAME),
discovery_info.get(CONF_HOST),
port)
# Ignore hub name when checking if this hub is known - ip and port only
if host and host[1:] in set([h[1:] for h in DEVICES]):
_LOGGER.debug("Discovered host already known: %s", host)
return
elif CONF_HOST in config:
host = (
config.get(CONF_NAME),
config.get(CONF_HOST),
config.get(CONF_PORT),
)
activity = config.get(ATTR_ACTIVITY)
else:
hass.data[CONF_DEVICE_CACHE].append(config)
return
name, address, port = host
_LOGGER.info("Loading Harmony Platform: %s at %s:%s, startup activity: %s",
name, address, port, activity)
try: try:
_LOGGER.debug("Calling pyharmony.ha_get_token for remote at: %s:%s", _LOGGER.debug("Calling pyharmony.ha_get_token for remote at: %s:%s",
host, port) address, port)
token = urllib.parse.quote_plus(pyharmony.ha_get_token(host, port)) token = urllib.parse.quote_plus(pyharmony.ha_get_token(address, port))
_LOGGER.debug("Received token: %s", token)
except ValueError as err: except ValueError as err:
_LOGGER.warning("%s for remote: %s", err.args[0], name) _LOGGER.warning("%s for remote: %s", err.args[0], name)
return False return False
_LOGGER.debug("Received token: %s", token) harmony_conf_file = hass.config.path(
DEVICES = [HarmonyRemote( '{}{}{}'.format('harmony_', slugify(name), '.conf'))
config.get(CONF_NAME), config.get(CONF_HOST), config.get(CONF_PORT), device = HarmonyRemote(
config.get(ATTR_ACTIVITY), harmony_conf_file, token)] name, address, port,
add_devices(DEVICES, True) activity, harmony_conf_file, token)
DEVICES.append(device)
add_devices([device])
register_services(hass) register_services(hass)
return True return True

View File

@ -10,9 +10,12 @@ import voluptuous as vol
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.util import slugify from homeassistant.util import slugify
from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.const import (
EVENT_HOMEASSISTANT_STOP,
ATTR_ENTITY_ID, TEMP_CELSIUS,
CONF_DEVICE_CLASS, CONF_COMMAND_ON, CONF_COMMAND_OFF
)
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.const import (ATTR_ENTITY_ID, TEMP_CELSIUS)
REQUIREMENTS = ['pyRFXtrx==0.18.0'] REQUIREMENTS = ['pyRFXtrx==0.18.0']
@ -27,7 +30,9 @@ ATTR_STATE = 'state'
ATTR_NAME = 'name' ATTR_NAME = 'name'
ATTR_FIREEVENT = 'fire_event' ATTR_FIREEVENT = 'fire_event'
ATTR_DATA_TYPE = 'data_type' ATTR_DATA_TYPE = 'data_type'
ATTR_DATA_BITS = 'data_bits'
ATTR_DUMMY = 'dummy' ATTR_DUMMY = 'dummy'
ATTR_OFF_DELAY = 'off_delay'
CONF_SIGNAL_REPETITIONS = 'signal_repetitions' CONF_SIGNAL_REPETITIONS = 'signal_repetitions'
CONF_DEVICES = 'devices' CONF_DEVICES = 'devices'
EVENT_BUTTON_PRESSED = 'button_pressed' EVENT_BUTTON_PRESSED = 'button_pressed'
@ -43,7 +48,8 @@ DATA_TYPES = OrderedDict([
('Total usage', 'W'), ('Total usage', 'W'),
('Sound', ''), ('Sound', ''),
('Sensor Status', ''), ('Sensor Status', ''),
('Counter value', '')]) ('Counter value', ''),
('UV', 'uv')])
RECEIVED_EVT_SUBSCRIBERS = [] RECEIVED_EVT_SUBSCRIBERS = []
RFX_DEVICES = {} RFX_DEVICES = {}
@ -77,6 +83,8 @@ def _valid_device(value, device_type):
if device_type == 'sensor': if device_type == 'sensor':
config[key] = DEVICE_SCHEMA_SENSOR(device) config[key] = DEVICE_SCHEMA_SENSOR(device)
elif device_type == 'binary_sensor':
config[key] = DEVICE_SCHEMA_BINARYSENSOR(device)
elif device_type == 'light_switch': elif device_type == 'light_switch':
config[key] = DEVICE_SCHEMA(device) config[key] = DEVICE_SCHEMA(device)
else: else:
@ -92,6 +100,11 @@ def valid_sensor(value):
return _valid_device(value, "sensor") return _valid_device(value, "sensor")
def valid_binary_sensor(value):
"""Validate binary sensor configuration."""
return _valid_device(value, "binary_sensor")
def _valid_light_switch(value): def _valid_light_switch(value):
return _valid_device(value, "light_switch") return _valid_device(value, "light_switch")
@ -108,6 +121,17 @@ DEVICE_SCHEMA_SENSOR = vol.Schema({
vol.All(cv.ensure_list, [vol.In(DATA_TYPES.keys())]), vol.All(cv.ensure_list, [vol.In(DATA_TYPES.keys())]),
}) })
DEVICE_SCHEMA_BINARYSENSOR = vol.Schema({
vol.Optional(ATTR_NAME, default=None): cv.string,
vol.Optional(CONF_DEVICE_CLASS, default=None): cv.string,
vol.Optional(ATTR_FIREEVENT, default=False): cv.boolean,
vol.Optional(ATTR_OFF_DELAY, default=None):
vol.Any(cv.time_period, cv.positive_timedelta),
vol.Optional(ATTR_DATA_BITS, default=None): cv.positive_int,
vol.Optional(CONF_COMMAND_ON, default=None): cv.byte,
vol.Optional(CONF_COMMAND_OFF, default=None): cv.byte
})
DEFAULT_SCHEMA = vol.Schema({ DEFAULT_SCHEMA = vol.Schema({
vol.Required("platform"): DOMAIN, vol.Required("platform"): DOMAIN,
vol.Optional(CONF_DEVICES, default={}): vol.All(dict, _valid_light_switch), vol.Optional(CONF_DEVICES, default={}): vol.All(dict, _valid_light_switch),
@ -191,6 +215,78 @@ def get_rfx_object(packetid):
return obj return obj
def get_pt2262_deviceid(device_id, nb_data_bits):
"""Extract and return the address bits from a Lighting4/PT2262 packet."""
import binascii
try:
data = bytearray.fromhex(device_id)
except ValueError:
return None
mask = 0xFF & ~((1 << nb_data_bits) - 1)
data[len(data)-1] &= mask
return binascii.hexlify(data)
def get_pt2262_cmd(device_id, data_bits):
"""Extract and return the data bits from a Lighting4/PT2262 packet."""
try:
data = bytearray.fromhex(device_id)
except ValueError:
return None
mask = 0xFF & ((1 << data_bits) - 1)
return hex(data[-1] & mask)
# pylint: disable=unused-variable
def get_pt2262_device(device_id):
"""Look for the device which id matches the given device_id parameter."""
for dev_id, device in RFX_DEVICES.items():
try:
if (device.is_pt2262 and
device.masked_id == get_pt2262_deviceid(
device_id,
device.data_bits)):
_LOGGER.info("rfxtrx: found matching device %s for %s",
device_id,
get_pt2262_deviceid(device_id, device.data_bits))
return device
except AttributeError:
continue
return None
# pylint: disable=unused-variable
def find_possible_pt2262_device(device_id):
"""Look for the device which id matches the given device_id parameter."""
for dev_id, device in RFX_DEVICES.items():
if len(dev_id) == len(device_id):
size = None
for i in range(0, len(dev_id)):
if dev_id[i] != device_id[i]:
break
size = i
if size is not None:
size = len(dev_id) - size - 1
_LOGGER.info("rfxtrx: found possible device %s for %s "
"with the following configuration:\n"
"data_bits=%d\n"
"command_on=0x%s\n"
"command_off=0x%s\n",
device_id,
dev_id,
size * 4,
dev_id[-size:], device_id[-size:])
return device
return None
def get_devices_from_config(config, device, hass): def get_devices_from_config(config, device, hass):
"""Read rfxtrx configuration.""" """Read rfxtrx configuration."""
signal_repetitions = config[CONF_SIGNAL_REPETITIONS] signal_repetitions = config[CONF_SIGNAL_REPETITIONS]
@ -318,6 +414,11 @@ class RfxtrxDevice(Entity):
"""Return is the device must fire event.""" """Return is the device must fire event."""
return self._should_fire_event return self._should_fire_event
@property
def is_pt2262(self):
"""Return true if the device is PT2262-based."""
return False
@property @property
def is_on(self): def is_on(self):
"""Return true if device is on.""" """Return true if device is on."""

View File

@ -102,7 +102,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
pin=pinnum, unit_of_measurement=pin.get( pin=pinnum, unit_of_measurement=pin.get(
CONF_UNIT_OF_MEASUREMENT), renderer=renderer)) CONF_UNIT_OF_MEASUREMENT), renderer=renderer))
add_devices(dev) add_devices(dev, True)
class ArestSensor(Entity): class ArestSensor(Entity):
@ -119,7 +119,6 @@ class ArestSensor(Entity):
self._state = STATE_UNKNOWN self._state = STATE_UNKNOWN
self._unit_of_measurement = unit_of_measurement self._unit_of_measurement = unit_of_measurement
self._renderer = renderer self._renderer = renderer
self.update()
if self._pin is not None: if self._pin is not None:
request = requests.get( request = requests.get(

View File

@ -0,0 +1,145 @@
"""
Support for BH1750 light sensor.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.bh1750/
"""
import asyncio
from functools import partial
import logging
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
import homeassistant.helpers.config_validation as cv
from homeassistant.const import CONF_NAME
from homeassistant.helpers.entity import Entity
REQUIREMENTS = ['i2csense==0.0.4',
'smbus-cffi==0.5.1']
_LOGGER = logging.getLogger(__name__)
CONF_I2C_ADDRESS = 'i2c_address'
CONF_I2C_BUS = 'i2c_bus'
CONF_OPERATION_MODE = 'operation_mode'
CONF_SENSITIVITY = 'sensitivity'
CONF_DELAY = 'measurement_delay_ms'
CONF_MULTIPLIER = 'multiplier'
# Operation modes for BH1750 sensor (from the datasheet). Time typically 120ms
# In one time measurements, device is set to Power Down after each sample.
CONTINUOUS_LOW_RES_MODE = "continuous_low_res_mode"
CONTINUOUS_HIGH_RES_MODE_1 = "continuous_high_res_mode_1"
CONTINUOUS_HIGH_RES_MODE_2 = "continuous_high_res_mode_2"
ONE_TIME_LOW_RES_MODE = "one_time_low_res_mode"
ONE_TIME_HIGH_RES_MODE_1 = "one_time_high_res_mode_1"
ONE_TIME_HIGH_RES_MODE_2 = "one_time_high_res_mode_2"
OPERATION_MODES = {
CONTINUOUS_LOW_RES_MODE: (0x13, True), # 4lx resolution
CONTINUOUS_HIGH_RES_MODE_1: (0x10, True), # 1lx resolution.
CONTINUOUS_HIGH_RES_MODE_2: (0X11, True), # 0.5lx resolution.
ONE_TIME_LOW_RES_MODE: (0x23, False), # 4lx resolution.
ONE_TIME_HIGH_RES_MODE_1: (0x20, False), # 1lx resolution.
ONE_TIME_HIGH_RES_MODE_2: (0x21, False), # 0.5lx resolution.
}
SENSOR_UNIT = 'lx'
DEFAULT_NAME = 'BH1750 Light Sensor'
DEFAULT_I2C_ADDRESS = '0x23'
DEFAULT_I2C_BUS = 1
DEFAULT_MODE = CONTINUOUS_HIGH_RES_MODE_1
DEFAULT_DELAY_MS = 120
DEFAULT_SENSITIVITY = 69 # from 31 to 254
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_I2C_ADDRESS, default=DEFAULT_I2C_ADDRESS): cv.string,
vol.Optional(CONF_I2C_BUS, default=DEFAULT_I2C_BUS): vol.Coerce(int),
vol.Optional(CONF_OPERATION_MODE, default=DEFAULT_MODE):
vol.In(OPERATION_MODES),
vol.Optional(CONF_SENSITIVITY, default=DEFAULT_SENSITIVITY):
cv.positive_int,
vol.Optional(CONF_DELAY, default=DEFAULT_DELAY_MS): cv.positive_int,
vol.Optional(CONF_MULTIPLIER, default=1.): vol.Range(min=0.1, max=10),
})
# pylint: disable=import-error
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up the BH1750 sensor."""
import smbus
from i2csense.bh1750 import BH1750
name = config.get(CONF_NAME)
bus_number = config.get(CONF_I2C_BUS)
i2c_address = config.get(CONF_I2C_ADDRESS)
operation_mode = config.get(CONF_OPERATION_MODE)
bus = smbus.SMBus(bus_number)
sensor = yield from hass.async_add_job(
partial(BH1750, bus, i2c_address,
operation_mode=operation_mode,
measurement_delay=config.get(CONF_DELAY),
sensitivity=config.get(CONF_SENSITIVITY),
logger=_LOGGER)
)
if not sensor.sample_ok:
_LOGGER.error("BH1750 sensor not detected at %s", i2c_address)
return False
dev = [BH1750Sensor(sensor, name, SENSOR_UNIT,
config.get(CONF_MULTIPLIER))]
_LOGGER.info("Setup of BH1750 light sensor at %s in mode %s is complete.",
i2c_address, operation_mode)
async_add_devices(dev)
class BH1750Sensor(Entity):
"""Implementation of the BH1750 sensor."""
def __init__(self, bh1750_sensor, name, unit, multiplier=1.):
"""Initialize the sensor."""
self._name = name
self._unit_of_measurement = unit
self._multiplier = multiplier
self.bh1750_sensor = bh1750_sensor
if self.bh1750_sensor.light_level >= 0:
self._state = int(round(self.bh1750_sensor.light_level))
else:
self._state = None
@property
def name(self) -> str:
"""Return the name of the sensor."""
return self._name
@property
def state(self) -> int:
"""Return the state of the sensor."""
return self._state
@property
def unit_of_measurement(self) -> str:
"""Return the unit of measurement of the sensor."""
return self._unit_of_measurement
@property
def device_class(self) -> str:
"""Return the class of this device, from component DEVICE_CLASSES."""
return 'light'
@asyncio.coroutine
def async_update(self):
"""Get the latest data from the BH1750 and update the states."""
yield from self.hass.async_add_job(self.bh1750_sensor.update)
if self.bh1750_sensor.sample_ok \
and self.bh1750_sensor.light_level >= 0:
self._state = int(round(self.bh1750_sensor.light_level
* self._multiplier))
else:
_LOGGER.warning("Bad Update of sensor.%s: %s",
self.name, self.bh1750_sensor.light_level)

Some files were not shown because too many files have changed in this diff Show More