mirror of
https://github.com/home-assistant/core.git
synced 2025-07-28 07:37:34 +00:00
commit
7461c57542
90
.coveragerc
90
.coveragerc
@ -35,23 +35,35 @@ omit =
|
||||
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/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/enocean.py
|
||||
homeassistant/components/*/enocean.py
|
||||
|
||||
homeassistant/components/envisalink.py
|
||||
homeassistant/components/*/envisalink.py
|
||||
|
||||
homeassistant/components/google.py
|
||||
homeassistant/components/*/google.py
|
||||
|
||||
homeassistant/components/insteon_hub.py
|
||||
homeassistant/components/*/insteon_hub.py
|
||||
homeassistant/components/hdmi_cec.py
|
||||
homeassistant/components/*/hdmi_cec.py
|
||||
|
||||
homeassistant/components/homematic.py
|
||||
homeassistant/components/*/homematic.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/joaoapps_join.py
|
||||
homeassistant/components/*/joaoapps_join.py
|
||||
|
||||
homeassistant/components/juicenet.py
|
||||
homeassistant/components/*/juicenet.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
|
||||
|
||||
@ -80,15 +98,27 @@ omit =
|
||||
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/mysensors.py
|
||||
homeassistant/components/*/mysensors.py
|
||||
|
||||
homeassistant/components/neato.py
|
||||
homeassistant/components/*/neato.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
|
||||
|
||||
@ -116,6 +146,9 @@ omit =
|
||||
homeassistant/components/scsgate.py
|
||||
homeassistant/components/*/scsgate.py
|
||||
|
||||
homeassistant/components/tado.py
|
||||
homeassistant/components/*/tado.py
|
||||
|
||||
homeassistant/components/tellduslive.py
|
||||
homeassistant/components/*/tellduslive.py
|
||||
|
||||
@ -148,45 +181,18 @@ omit =
|
||||
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/maxcube.py
|
||||
homeassistant/components/*/maxcube.py
|
||||
|
||||
homeassistant/components/tado.py
|
||||
homeassistant/components/*/tado.py
|
||||
|
||||
homeassistant/components/zha/__init__.py
|
||||
homeassistant/components/zha/const.py
|
||||
homeassistant/components/*/zha.py
|
||||
|
||||
homeassistant/components/eight_sleep.py
|
||||
homeassistant/components/*/eight_sleep.py
|
||||
homeassistant/components/zigbee.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/concord232.py
|
||||
@ -224,11 +230,11 @@ omit =
|
||||
homeassistant/components/climate/sensibo.py
|
||||
homeassistant/components/cover/garadget.py
|
||||
homeassistant/components/cover/homematic.py
|
||||
homeassistant/components/cover/knx.py
|
||||
homeassistant/components/cover/myq.py
|
||||
homeassistant/components/cover/opengarage.py
|
||||
homeassistant/components/cover/rpi_gpio.py
|
||||
homeassistant/components/cover/scsgate.py
|
||||
homeassistant/components/cover/wink.py
|
||||
homeassistant/components/device_tracker/actiontec.py
|
||||
homeassistant/components/device_tracker/aruba.py
|
||||
homeassistant/components/device_tracker/asuswrt.py
|
||||
@ -242,6 +248,7 @@ omit =
|
||||
homeassistant/components/device_tracker/gpslogger.py
|
||||
homeassistant/components/device_tracker/icloud.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/mikrotik.py
|
||||
homeassistant/components/device_tracker/netgear.py
|
||||
@ -263,12 +270,10 @@ omit =
|
||||
homeassistant/components/fan/mqtt.py
|
||||
homeassistant/components/feedreader.py
|
||||
homeassistant/components/foursquare.py
|
||||
homeassistant/components/hdmi_cec.py
|
||||
homeassistant/components/ifttt.py
|
||||
homeassistant/components/image_processing/dlib_face_detect.py
|
||||
homeassistant/components/image_processing/dlib_face_identify.py
|
||||
homeassistant/components/image_processing/seven_segments.py
|
||||
homeassistant/components/joaoapps_join.py
|
||||
homeassistant/components/keyboard.py
|
||||
homeassistant/components/keyboard_remote.py
|
||||
homeassistant/components/light/avion.py
|
||||
@ -278,7 +283,7 @@ omit =
|
||||
homeassistant/components/light/flux_led.py
|
||||
homeassistant/components/light/hue.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/limitlessled.py
|
||||
homeassistant/components/light/mystrom.py
|
||||
@ -312,7 +317,6 @@ omit =
|
||||
homeassistant/components/media_player/frontier_silicon.py
|
||||
homeassistant/components/media_player/gpmdp.py
|
||||
homeassistant/components/media_player/gstreamer.py
|
||||
homeassistant/components/media_player/hdmi_cec.py
|
||||
homeassistant/components/media_player/itunes.py
|
||||
homeassistant/components/media_player/kodi.py
|
||||
homeassistant/components/media_player/lg_netcast.py
|
||||
@ -342,13 +346,13 @@ omit =
|
||||
homeassistant/components/notify/aws_sns.py
|
||||
homeassistant/components/notify/aws_sqs.py
|
||||
homeassistant/components/notify/ciscospark.py
|
||||
homeassistant/components/notify/clicksend.py
|
||||
homeassistant/components/notify/discord.py
|
||||
homeassistant/components/notify/facebook.py
|
||||
homeassistant/components/notify/free_mobile.py
|
||||
homeassistant/components/notify/gntp.py
|
||||
homeassistant/components/notify/group.py
|
||||
homeassistant/components/notify/instapush.py
|
||||
homeassistant/components/notify/joaoapps_join.py
|
||||
homeassistant/components/notify/kodi.py
|
||||
homeassistant/components/notify/lannouncer.py
|
||||
homeassistant/components/notify/llamalab_automate.py
|
||||
@ -379,8 +383,10 @@ omit =
|
||||
homeassistant/components/sensor/arest.py
|
||||
homeassistant/components/sensor/arwn.py
|
||||
homeassistant/components/sensor/bbox.py
|
||||
homeassistant/components/sensor/bh1750.py
|
||||
homeassistant/components/sensor/bitcoin.py
|
||||
homeassistant/components/sensor/blockchain.py
|
||||
homeassistant/components/sensor/bme280.py
|
||||
homeassistant/components/sensor/bom.py
|
||||
homeassistant/components/sensor/broadlink.py
|
||||
homeassistant/components/sensor/buienradar.py
|
||||
@ -419,6 +425,7 @@ omit =
|
||||
homeassistant/components/sensor/haveibeenpwned.py
|
||||
homeassistant/components/sensor/hddtemp.py
|
||||
homeassistant/components/sensor/hp_ilo.py
|
||||
homeassistant/components/sensor/htu21d.py
|
||||
homeassistant/components/sensor/hydroquebec.py
|
||||
homeassistant/components/sensor/imap.py
|
||||
homeassistant/components/sensor/imap_email_content.py
|
||||
@ -473,6 +480,7 @@ omit =
|
||||
homeassistant/components/sensor/transmission.py
|
||||
homeassistant/components/sensor/twitch.py
|
||||
homeassistant/components/sensor/uber.py
|
||||
homeassistant/components/sensor/upnp.py
|
||||
homeassistant/components/sensor/ups.py
|
||||
homeassistant/components/sensor/usps.py
|
||||
homeassistant/components/sensor/vasttrafik.py
|
||||
@ -480,6 +488,7 @@ omit =
|
||||
homeassistant/components/sensor/xbox_live.py
|
||||
homeassistant/components/sensor/yweather.py
|
||||
homeassistant/components/sensor/zamg.py
|
||||
homeassistant/components/shiftr.py
|
||||
homeassistant/components/spc.py
|
||||
homeassistant/components/switch/acer_projector.py
|
||||
homeassistant/components/switch/anel_pwrctrl.py
|
||||
@ -489,7 +498,6 @@ omit =
|
||||
homeassistant/components/switch/dlink.py
|
||||
homeassistant/components/switch/edimax.py
|
||||
homeassistant/components/switch/fritzdect.py
|
||||
homeassistant/components/switch/hdmi_cec.py
|
||||
homeassistant/components/switch/hikvisioncam.py
|
||||
homeassistant/components/switch/hook.py
|
||||
homeassistant/components/switch/kankun.py
|
||||
|
@ -1,2 +1,14 @@
|
||||
.tox
|
||||
# General files
|
||||
.git
|
||||
.github
|
||||
config
|
||||
|
||||
# Test related files
|
||||
.tox
|
||||
|
||||
# Other virtualization methods
|
||||
venv
|
||||
.vagrant
|
||||
|
||||
# Temporary files
|
||||
**/__pycache__
|
10
Dockerfile
10
Dockerfile
@ -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
|
||||
MAINTAINER Paulus Schoutsen <Paulus@PaulusSchoutsen.nl>
|
||||
|
||||
@ -21,8 +25,12 @@ RUN virtualization/Docker/setup_docker_prereqs
|
||||
|
||||
# Install hass component dependencies
|
||||
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 && \
|
||||
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 . .
|
||||
|
@ -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/
|
||||
"""
|
||||
import logging
|
||||
from time import sleep
|
||||
|
||||
import homeassistant.components.alarm_control_panel as alarm
|
||||
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."""
|
||||
alarms = []
|
||||
if int(hub.config.get(CONF_ALARM, 1)):
|
||||
hub.update_alarms()
|
||||
alarms.extend([
|
||||
VerisureAlarm(value.id)
|
||||
for value in hub.alarm_status.values()
|
||||
])
|
||||
hub.update_overview()
|
||||
alarms.append(VerisureAlarm())
|
||||
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):
|
||||
"""Representation of a Verisure alarm status."""
|
||||
|
||||
def __init__(self, device_id):
|
||||
"""Initialize the Verisure alarm panel."""
|
||||
self._id = device_id
|
||||
def __init__(self):
|
||||
"""Initalize the Verisure alarm panel."""
|
||||
self._state = STATE_UNKNOWN
|
||||
self._digits = hub.config.get(CONF_CODE_DIGITS)
|
||||
self._changed_by = None
|
||||
@ -41,18 +51,13 @@ class VerisureAlarm(alarm.AlarmControlPanel):
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
return 'Alarm {}'.format(self._id)
|
||||
return '{} alarm'.format(hub.session.installations[0]['alias'])
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return True if entity is available."""
|
||||
return hub.available
|
||||
|
||||
@property
|
||||
def code_format(self):
|
||||
"""Return the code format as regex."""
|
||||
@ -65,33 +70,26 @@ class VerisureAlarm(alarm.AlarmControlPanel):
|
||||
|
||||
def update(self):
|
||||
"""Update alarm status."""
|
||||
hub.update_alarms()
|
||||
|
||||
if hub.alarm_status[self._id].status == 'unarmed':
|
||||
hub.update_overview()
|
||||
status = hub.get_first("$.armState.statusType")
|
||||
if status == 'DISARMED':
|
||||
self._state = STATE_ALARM_DISARMED
|
||||
elif hub.alarm_status[self._id].status == 'armedhome':
|
||||
elif status == '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
|
||||
elif hub.alarm_status[self._id].status != 'pending':
|
||||
_LOGGER.error(
|
||||
"Unknown alarm state %s", hub.alarm_status[self._id].status)
|
||||
self._changed_by = hub.alarm_status[self._id].name
|
||||
elif status != 'PENDING':
|
||||
_LOGGER.error('Unknown alarm state %s', status)
|
||||
self._changed_by = hub.get_first("$.armState.name")
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
hub.my_pages.alarm.set(code, 'DISARMED')
|
||||
_LOGGER.info("Verisure alarm disarming")
|
||||
hub.my_pages.alarm.wait_while_pending()
|
||||
set_arm_state('DISARMED', code)
|
||||
|
||||
def alarm_arm_home(self, code=None):
|
||||
"""Send arm home command."""
|
||||
hub.my_pages.alarm.set(code, 'ARMED_HOME')
|
||||
_LOGGER.info("Verisure alarm arming home")
|
||||
hub.my_pages.alarm.wait_while_pending()
|
||||
set_arm_state('ARMED_HOME', code)
|
||||
|
||||
def alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
hub.my_pages.alarm.set(code, 'ARMED_AWAY')
|
||||
_LOGGER.info("Verisure alarm arming away")
|
||||
hub.my_pages.alarm.wait_while_pending()
|
||||
set_arm_state('ARMED_AWAY', code)
|
||||
|
@ -25,6 +25,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
DOMAIN = 'alert'
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
|
||||
CONF_DONE_MESSAGE = 'done_message'
|
||||
CONF_CAN_ACK = 'can_acknowledge'
|
||||
CONF_NOTIFIERS = 'notifiers'
|
||||
CONF_REPEAT = 'repeat'
|
||||
@ -35,6 +36,7 @@ DEFAULT_SKIP_FIRST = False
|
||||
|
||||
ALERT_SCHEMA = vol.Schema({
|
||||
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_STATE, default=STATE_ON): cv.string,
|
||||
vol.Required(CONF_REPEAT): vol.All(cv.ensure_list, [vol.Coerce(float)]),
|
||||
@ -121,10 +123,10 @@ def async_setup(hass, config):
|
||||
# Setup alerts
|
||||
for entity_id, alert in alerts.items():
|
||||
entity = Alert(hass, entity_id,
|
||||
alert[CONF_NAME], alert[CONF_ENTITY_ID],
|
||||
alert[CONF_STATE], alert[CONF_REPEAT],
|
||||
alert[CONF_SKIP_FIRST], alert[CONF_NOTIFIERS],
|
||||
alert[CONF_CAN_ACK])
|
||||
alert[CONF_NAME], alert[CONF_DONE_MESSAGE],
|
||||
alert[CONF_ENTITY_ID], alert[CONF_STATE],
|
||||
alert[CONF_REPEAT], alert[CONF_SKIP_FIRST],
|
||||
alert[CONF_NOTIFIERS], alert[CONF_CAN_ACK])
|
||||
all_alerts[entity.entity_id] = entity
|
||||
|
||||
# Read descriptions
|
||||
@ -154,8 +156,8 @@ def async_setup(hass, config):
|
||||
class Alert(ToggleEntity):
|
||||
"""Representation of an alert."""
|
||||
|
||||
def __init__(self, hass, entity_id, name, watched_entity_id, state,
|
||||
repeat, skip_first, notifiers, can_ack):
|
||||
def __init__(self, hass, entity_id, name, done_message, watched_entity_id,
|
||||
state, repeat, skip_first, notifiers, can_ack):
|
||||
"""Initialize the alert."""
|
||||
self.hass = hass
|
||||
self._name = name
|
||||
@ -163,6 +165,7 @@ class Alert(ToggleEntity):
|
||||
self._skip_first = skip_first
|
||||
self._notifiers = notifiers
|
||||
self._can_ack = can_ack
|
||||
self._done_message = done_message
|
||||
|
||||
self._delay = [timedelta(minutes=val) for val in repeat]
|
||||
self._next_delay = 0
|
||||
@ -170,6 +173,7 @@ class Alert(ToggleEntity):
|
||||
self._firing = False
|
||||
self._ack = False
|
||||
self._cancel = None
|
||||
self._send_done_message = False
|
||||
self.entity_id = ENTITY_ID_FORMAT.format(entity_id)
|
||||
|
||||
event.async_track_state_change(
|
||||
@ -230,6 +234,8 @@ class Alert(ToggleEntity):
|
||||
self._cancel()
|
||||
self._ack = 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)
|
||||
|
||||
@asyncio.coroutine
|
||||
@ -249,11 +255,21 @@ class Alert(ToggleEntity):
|
||||
|
||||
if not self._ack:
|
||||
_LOGGER.info("Alerting: %s", self._name)
|
||||
self._send_done_message = True
|
||||
for target in self._notifiers:
|
||||
yield from self.hass.services.async_call(
|
||||
'notify', target, {'message': self._name})
|
||||
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
|
||||
def async_turn_on(self):
|
||||
"""Async Unacknowledge alert."""
|
||||
|
@ -1,27 +1,27 @@
|
||||
"""
|
||||
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/
|
||||
"""
|
||||
import logging
|
||||
|
||||
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
|
||||
|
||||
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']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_ATTRIBUTION = 'Data provided by arlo.netgear.com'
|
||||
|
||||
DOMAIN = 'arlo'
|
||||
CONF_ATTRIBUTION = "Data provided by arlo.netgear.com"
|
||||
|
||||
DATA_ARLO = 'data_arlo'
|
||||
DEFAULT_BRAND = 'Netgear Arlo'
|
||||
DOMAIN = 'arlo'
|
||||
|
||||
NOTIFICATION_ID = 'arlo_notification'
|
||||
NOTIFICATION_TITLE = 'Arlo Camera Setup'
|
||||
@ -47,7 +47,7 @@ def setup(hass, config):
|
||||
arlo = PyArlo(username, password, preload=False)
|
||||
if not arlo.is_connected:
|
||||
return False
|
||||
hass.data['arlo'] = arlo
|
||||
hass.data[DATA_ARLO] = arlo
|
||||
except (ConnectTimeout, HTTPError) as ex:
|
||||
_LOGGER.error("Unable to connect to Netgar Arlo: %s", str(ex))
|
||||
persistent_notification.create(
|
||||
|
@ -11,6 +11,7 @@ import os
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.const import (ATTR_LOCATION, ATTR_TRIPPED,
|
||||
CONF_HOST, CONF_INCLUDE, CONF_NAME,
|
||||
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.helpers import config_validation as cv
|
||||
from homeassistant.helpers import discovery
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.loader import get_component
|
||||
|
||||
|
||||
REQUIREMENTS = ['axis==7']
|
||||
REQUIREMENTS = ['axis==8']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -59,6 +61,21 @@ CONFIG_SCHEMA = vol.Schema({
|
||||
}),
|
||||
}, 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):
|
||||
"""Request configuration steps from the user."""
|
||||
@ -135,23 +152,34 @@ def setup(hass, base_config):
|
||||
|
||||
def axis_device_discovered(service, discovery_info):
|
||||
"""Called when axis devices has been found."""
|
||||
host = discovery_info['host']
|
||||
host = discovery_info[CONF_HOST]
|
||||
name = discovery_info['hostname']
|
||||
serialnumber = discovery_info['properties']['macaddress']
|
||||
|
||||
if serialnumber not in AXIS_DEVICES:
|
||||
config_file = _read_config(hass)
|
||||
if serialnumber in config_file:
|
||||
# Device config saved to file
|
||||
try:
|
||||
config = DEVICE_SCHEMA(config_file[serialnumber])
|
||||
config[CONF_HOST] = host
|
||||
except vol.Invalid as err:
|
||||
_LOGGER.error("Bad data from %s. %s", CONFIG_FILE, err)
|
||||
return False
|
||||
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:
|
||||
# New device, create configuration request for UI
|
||||
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)
|
||||
|
||||
if DOMAIN in base_config:
|
||||
@ -160,7 +188,30 @@ def setup(hass, base_config):
|
||||
if CONF_NAME not in config:
|
||||
config[CONF_NAME] = device
|
||||
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
|
||||
|
||||
@ -190,8 +241,16 @@ def setup_device(hass, config):
|
||||
|
||||
if enable_metadatastream:
|
||||
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
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@ -311,4 +370,4 @@ REMAP = [{'type': 'motion',
|
||||
'class': 'input',
|
||||
'topic': 'tns1:Device/tnsaxis:IO/Port',
|
||||
'subscribe': 'onvif:Device/axis:IO/Port',
|
||||
'platform': 'sensor'}, ]
|
||||
'platform': 'binary_sensor'}, ]
|
||||
|
@ -51,7 +51,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
||||
add_devices([ArestBinarySensor(
|
||||
arest, resource, config.get(CONF_NAME, response[CONF_NAME]),
|
||||
device_class, pin)])
|
||||
device_class, pin)], True)
|
||||
|
||||
|
||||
class ArestBinarySensor(BinarySensorDevice):
|
||||
@ -64,7 +64,6 @@ class ArestBinarySensor(BinarySensorDevice):
|
||||
self._name = name
|
||||
self._device_class = device_class
|
||||
self._pin = pin
|
||||
self.update()
|
||||
|
||||
if self._pin is not None:
|
||||
request = requests.get(
|
||||
|
@ -8,19 +8,18 @@ import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, PLATFORM_SCHEMA)
|
||||
from homeassistant.components.digital_ocean import (
|
||||
CONF_DROPLETS, ATTR_CREATED_AT, ATTR_DROPLET_ID, ATTR_DROPLET_NAME,
|
||||
ATTR_FEATURES, ATTR_IPV4_ADDRESS, ATTR_IPV6_ADDRESS, ATTR_MEMORY,
|
||||
ATTR_REGION, ATTR_VCPUS)
|
||||
from homeassistant.loader import get_component
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
ATTR_REGION, ATTR_VCPUS, DATA_DIGITAL_OCEAN)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_NAME = 'Droplet'
|
||||
DEFAULT_SENSOR_CLASS = 'motion'
|
||||
DEFAULT_SENSOR_CLASS = 'moving'
|
||||
DEPENDENCIES = ['digital_ocean']
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
@ -30,19 +29,21 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""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)
|
||||
|
||||
dev = []
|
||||
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:
|
||||
_LOGGER.error("Droplet %s is not available", droplet)
|
||||
return False
|
||||
dev.append(DigitalOceanBinarySensor(
|
||||
digital_ocean.DIGITAL_OCEAN, droplet_id))
|
||||
dev.append(DigitalOceanBinarySensor(digital, droplet_id))
|
||||
|
||||
add_devices(dev)
|
||||
add_devices(dev, True)
|
||||
|
||||
|
||||
class DigitalOceanBinarySensor(BinarySensorDevice):
|
||||
@ -53,7 +54,7 @@ class DigitalOceanBinarySensor(BinarySensorDevice):
|
||||
self._digital_ocean = do
|
||||
self._droplet_id = droplet_id
|
||||
self._state = None
|
||||
self.update()
|
||||
self.data = None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
@ -50,6 +50,10 @@ class ModbusCoilSensor(BinarySensorDevice):
|
||||
self._coil = int(coil)
|
||||
self._value = None
|
||||
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return the state of the sensor."""
|
||||
|
233
homeassistant/components/binary_sensor/rfxtrx.py
Normal file
233
homeassistant/components/binary_sensor/rfxtrx.py
Normal 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()
|
59
homeassistant/components/binary_sensor/verisure.py
Normal file
59
homeassistant/components/binary_sensor/verisure.py
Normal 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()
|
@ -30,7 +30,7 @@ def setup_platform(hass, config, add_devices, disc_info=None):
|
||||
if disc_info is None:
|
||||
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
|
||||
|
||||
calendar_service = GoogleCalendarService(hass.config.path(TOKEN_FILE))
|
||||
|
@ -12,13 +12,16 @@ from datetime import timedelta
|
||||
import logging
|
||||
import hashlib
|
||||
from random import SystemRandom
|
||||
import os
|
||||
|
||||
import aiohttp
|
||||
from aiohttp import web
|
||||
import async_timeout
|
||||
import voluptuous as vol
|
||||
|
||||
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.helpers.aiohttp_client import async_get_clientsession
|
||||
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.components.http import HomeAssistantView, KEY_AUTHENTICATED
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SERVICE_EN_MOTION = 'enable_motion_detection'
|
||||
SERVICE_DISEN_MOTION = 'disable_motion_detection'
|
||||
DOMAIN = 'camera'
|
||||
DEPENDENCIES = ['http']
|
||||
SCAN_INTERVAL = timedelta(seconds=30)
|
||||
@ -38,11 +44,30 @@ STATE_RECORDING = 'recording'
|
||||
STATE_STREAMING = 'streaming'
|
||||
STATE_IDLE = 'idle'
|
||||
|
||||
DEFAULT_CONTENT_TYPE = 'image/jpeg'
|
||||
ENTITY_IMAGE_URL = '/api/camera_proxy/{0}?token={1}'
|
||||
|
||||
TOKEN_CHANGE_INTERVAL = timedelta(minutes=5)
|
||||
_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
|
||||
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())
|
||||
|
||||
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
|
||||
|
||||
|
||||
@ -101,6 +164,7 @@ class Camera(Entity):
|
||||
def __init__(self):
|
||||
"""Initialize a camera."""
|
||||
self.is_streaming = False
|
||||
self.content_type = DEFAULT_CONTENT_TYPE
|
||||
self.access_tokens = collections.deque([], 2)
|
||||
self.async_update_token()
|
||||
|
||||
@ -124,6 +188,11 @@ class Camera(Entity):
|
||||
"""Return the camera brand."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def motion_detection_enabled(self):
|
||||
"""Return the camera motion detection status."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def model(self):
|
||||
"""Return the camera model."""
|
||||
@ -149,16 +218,17 @@ class Camera(Entity):
|
||||
response = web.StreamResponse()
|
||||
|
||||
response.content_type = ('multipart/x-mixed-replace; '
|
||||
'boundary=--jpegboundary')
|
||||
'boundary=--frameboundary')
|
||||
yield from response.prepare(request)
|
||||
|
||||
def write(img_bytes):
|
||||
"""Write image to stream."""
|
||||
response.write(bytes(
|
||||
'--jpegboundary\r\n'
|
||||
'Content-Type: image/jpeg\r\n'
|
||||
'--frameboundary\r\n'
|
||||
'Content-Type: {}\r\n'
|
||||
'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
|
||||
|
||||
@ -199,6 +269,22 @@ class Camera(Entity):
|
||||
else:
|
||||
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
|
||||
def state_attributes(self):
|
||||
"""Return the camera state attributes."""
|
||||
@ -212,6 +298,9 @@ class Camera(Entity):
|
||||
if self.brand:
|
||||
attr['brand'] = self.brand
|
||||
|
||||
if self.motion_detection_enabled:
|
||||
attr['motion_detection'] = self.motion_detection_enabled
|
||||
|
||||
return attr
|
||||
|
||||
@callback
|
||||
@ -269,7 +358,8 @@ class CameraImageView(CameraView):
|
||||
image = yield from camera.async_camera_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)
|
||||
|
||||
|
@ -6,32 +6,32 @@ https://home-assistant.io/components/camera.arlo/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.components.arlo import DEFAULT_BRAND
|
||||
|
||||
from homeassistant.components.camera import (Camera, PLATFORM_SCHEMA)
|
||||
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.ffmpeg import DATA_FFMPEG
|
||||
from homeassistant.helpers.aiohttp_client import (
|
||||
async_aiohttp_proxy_stream)
|
||||
|
||||
DEPENDENCIES = ['arlo', 'ffmpeg']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_FFMPEG_ARGUMENTS = 'ffmpeg_arguments'
|
||||
ARLO_MODE_ARMED = 'armed'
|
||||
ARLO_MODE_DISARMED = 'disarmed'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_FFMPEG_ARGUMENTS):
|
||||
cv.string,
|
||||
vol.Optional(CONF_FFMPEG_ARGUMENTS): cv.string,
|
||||
})
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Set up an Arlo IP Camera."""
|
||||
arlo = hass.data.get('arlo')
|
||||
arlo = hass.data.get(DATA_ARLO)
|
||||
if not arlo:
|
||||
return False
|
||||
|
||||
@ -40,7 +40,6 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
cameras.append(ArloCam(hass, camera, config))
|
||||
|
||||
async_add_devices(cameras, True)
|
||||
return True
|
||||
|
||||
|
||||
class ArloCam(Camera):
|
||||
@ -49,14 +48,15 @@ class ArloCam(Camera):
|
||||
def __init__(self, hass, camera, device_info):
|
||||
"""Initialize an Arlo camera."""
|
||||
super().__init__()
|
||||
|
||||
self._camera = camera
|
||||
self._base_stn = hass.data['arlo'].base_stations[0]
|
||||
self._name = self._camera.name
|
||||
self._motion_status = False
|
||||
self._ffmpeg = hass.data[DATA_FFMPEG]
|
||||
self._ffmpeg_arguments = device_info.get(CONF_FFMPEG_ARGUMENTS)
|
||||
|
||||
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
|
||||
|
||||
@asyncio.coroutine
|
||||
@ -90,3 +90,27 @@ class ArloCam(Camera):
|
||||
def brand(self):
|
||||
"""Camera 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)
|
||||
|
@ -7,15 +7,16 @@ https://home-assistant.io/components/camera.axis/
|
||||
import logging
|
||||
|
||||
from homeassistant.const import (
|
||||
CONF_NAME, CONF_USERNAME, CONF_PASSWORD,
|
||||
CONF_HOST, CONF_NAME, CONF_USERNAME, CONF_PASSWORD,
|
||||
CONF_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION)
|
||||
from homeassistant.components.camera.mjpeg import (
|
||||
CONF_MJPEG_URL, CONF_STILL_IMAGE_URL, MjpegCamera)
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['axis']
|
||||
DOMAIN = 'axis'
|
||||
DEPENDENCIES = [DOMAIN]
|
||||
|
||||
|
||||
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):
|
||||
"""Setup Axis camera."""
|
||||
device_info = {
|
||||
CONF_NAME: discovery_info['name'],
|
||||
CONF_USERNAME: discovery_info['username'],
|
||||
CONF_PASSWORD: discovery_info['password'],
|
||||
CONF_MJPEG_URL: _get_image_url(discovery_info['host'], 'mjpeg'),
|
||||
CONF_STILL_IMAGE_URL: _get_image_url(discovery_info['host'], 'single'),
|
||||
config = {
|
||||
CONF_NAME: discovery_info[CONF_NAME],
|
||||
CONF_USERNAME: discovery_info[CONF_USERNAME],
|
||||
CONF_PASSWORD: discovery_info[CONF_PASSWORD],
|
||||
CONF_MJPEG_URL: _get_image_url(discovery_info[CONF_HOST], 'mjpeg'),
|
||||
CONF_STILL_IMAGE_URL: _get_image_url(discovery_info[CONF_HOST],
|
||||
'single'),
|
||||
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')
|
||||
|
@ -5,25 +5,29 @@ For more details about this platform, please refer to the documentation
|
||||
https://home-assistant.io/components/demo/
|
||||
"""
|
||||
import os
|
||||
|
||||
import logging
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.components.camera import Camera
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Demo camera platform."""
|
||||
add_devices([
|
||||
DemoCamera('Demo camera')
|
||||
DemoCamera(hass, config, 'Demo camera')
|
||||
])
|
||||
|
||||
|
||||
class DemoCamera(Camera):
|
||||
"""The representation of a Demo camera."""
|
||||
|
||||
def __init__(self, name):
|
||||
def __init__(self, hass, config, name):
|
||||
"""Initialize demo camera component."""
|
||||
super().__init__()
|
||||
self._parent = hass
|
||||
self._name = name
|
||||
self._motion_status = False
|
||||
|
||||
def camera_image(self):
|
||||
"""Return a faked still image response."""
|
||||
@ -38,3 +42,21 @@ class DemoCamera(Camera):
|
||||
def name(self):
|
||||
"""Return the name of this camera."""
|
||||
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
|
||||
|
@ -17,13 +17,15 @@ from homeassistant.const import (
|
||||
CONF_NAME, CONF_USERNAME, CONF_PASSWORD, CONF_AUTHENTICATION,
|
||||
HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION)
|
||||
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 import config_validation as cv
|
||||
from homeassistant.util.async import run_coroutine_threadsafe
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_CONTENT_TYPE = 'content_type'
|
||||
CONF_LIMIT_REFETCH_TO_URL_CHANGE = 'limit_refetch_to_url_change'
|
||||
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_PASSWORD): 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.hass = hass
|
||||
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)
|
||||
password = device_info.get(CONF_PASSWORD)
|
||||
|
@ -5,6 +5,7 @@ For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/camera.local_file/
|
||||
"""
|
||||
import logging
|
||||
import mimetypes
|
||||
import os
|
||||
|
||||
import voluptuous as vol
|
||||
@ -46,6 +47,10 @@ class LocalFile(Camera):
|
||||
|
||||
self._name = name
|
||||
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):
|
||||
"""Return image response."""
|
||||
|
17
homeassistant/components/camera/services.yaml
Normal file
17
homeassistant/components/camera/services.yaml
Normal 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'
|
@ -24,22 +24,23 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
if not os.access(directory_path, os.R_OK):
|
||||
_LOGGER.error("file path %s is not readable", directory_path)
|
||||
return False
|
||||
hub.update_smartcam()
|
||||
hub.update_overview()
|
||||
smartcams = []
|
||||
smartcams.extend([
|
||||
VerisureSmartcam(hass, value.deviceLabel, directory_path)
|
||||
for value in hub.smartcam_status.values()])
|
||||
VerisureSmartcam(hass, device_label, directory_path)
|
||||
for device_label in hub.get(
|
||||
"$.customerImageCameras[*].deviceLabel")])
|
||||
add_devices(smartcams)
|
||||
|
||||
|
||||
class VerisureSmartcam(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."""
|
||||
super().__init__()
|
||||
|
||||
self._device_id = device_id
|
||||
self._device_label = device_label
|
||||
self._directory_path = directory_path
|
||||
self._image = None
|
||||
self._image_id = None
|
||||
@ -58,28 +59,27 @@ class VerisureSmartcam(Camera):
|
||||
|
||||
def check_imagelist(self):
|
||||
"""Check the contents of the image list."""
|
||||
hub.update_smartcam_imagelist()
|
||||
if (self._device_id not in hub.smartcam_dict or
|
||||
not hub.smartcam_dict[self._device_id]):
|
||||
hub.update_smartcam_imageseries()
|
||||
image_ids = hub.get_image_info(
|
||||
"$.imageSeries[?(@.deviceLabel=='%s')].image[0].imageId",
|
||||
self._device_label)
|
||||
if not image_ids:
|
||||
return
|
||||
images = hub.smartcam_dict[self._device_id]
|
||||
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)
|
||||
new_image_id = image_ids[0]
|
||||
if (new_image_id == '-1' or
|
||||
self._image_id == new_image_id):
|
||||
_LOGGER.debug("The image is the same, or loading image_id")
|
||||
return
|
||||
_LOGGER.debug("Download new image %s", new_image_id)
|
||||
hub.my_pages.smartcam.download_image(
|
||||
self._device_id, new_image_id, self._directory_path)
|
||||
new_image_path = os.path.join(
|
||||
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)
|
||||
self.delete_image(self)
|
||||
|
||||
self._image_id = new_image_id
|
||||
self._image = os.path.join(
|
||||
self._directory_path, '{}{}'.format(self._image_id, '.jpg'))
|
||||
self._image = new_image_path
|
||||
|
||||
def delete_image(self, event):
|
||||
"""Delete an old image."""
|
||||
@ -95,4 +95,6 @@ class VerisureSmartcam(Camera):
|
||||
@property
|
||||
def name(self):
|
||||
"""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)
|
||||
|
@ -693,8 +693,14 @@ class ClimateDevice(Entity):
|
||||
|
||||
def _convert_for_display(self, temp):
|
||||
"""Convert temperature into preferred units for display purposes."""
|
||||
if temp is None or not isinstance(temp, Number):
|
||||
if temp is None:
|
||||
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:
|
||||
temp = convert_temperature(
|
||||
temp, self.temperature_unit, self.unit_of_measurement)
|
||||
|
@ -67,7 +67,12 @@ class MySensorsHVAC(mysensors.MySensorsDeviceEntity, ClimateDevice):
|
||||
@property
|
||||
def current_temperature(self):
|
||||
"""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
|
||||
def target_temperature(self):
|
||||
@ -79,21 +84,21 @@ class MySensorsHVAC(mysensors.MySensorsDeviceEntity, ClimateDevice):
|
||||
temp = self._values.get(set_req.V_HVAC_SETPOINT_COOL)
|
||||
if temp is None:
|
||||
temp = self._values.get(set_req.V_HVAC_SETPOINT_HEAT)
|
||||
return temp
|
||||
return float(temp)
|
||||
|
||||
@property
|
||||
def target_temperature_high(self):
|
||||
"""Return the highbound target temperature we try to reach."""
|
||||
set_req = self.gateway.const.SetReq
|
||||
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
|
||||
def target_temperature_low(self):
|
||||
"""Return the lowbound target temperature we try to reach."""
|
||||
set_req = self.gateway.const.SetReq
|
||||
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
|
||||
def current_operation(self):
|
||||
|
@ -15,7 +15,7 @@ from homeassistant.components.climate import (
|
||||
from homeassistant.const import CONF_HOST, TEMP_FAHRENHEIT, ATTR_TEMPERATURE
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['radiotherm==1.2']
|
||||
REQUIREMENTS = ['radiotherm==1.3']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -84,6 +84,7 @@ class RadioThermostat(ClimateDevice):
|
||||
self._name = None
|
||||
self._fmode = None
|
||||
self._tmode = None
|
||||
self._tstate = None
|
||||
self._hold_temp = hold_temp
|
||||
self._away = False
|
||||
self._away_temps = away_temps
|
||||
@ -140,6 +141,7 @@ class RadioThermostat(ClimateDevice):
|
||||
self._name = self.device.name['raw']
|
||||
self._fmode = self.device.fmode['human']
|
||||
self._tmode = self.device.tmode['human']
|
||||
self._tstate = self.device.tstate['human']
|
||||
|
||||
if self._tmode == 'Cool':
|
||||
self._target_temperature = self.device.t_cool['raw']
|
||||
@ -147,6 +149,12 @@ class RadioThermostat(ClimateDevice):
|
||||
elif self._tmode == 'Heat':
|
||||
self._target_temperature = self.device.t_heat['raw']
|
||||
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:
|
||||
self._current_operation = STATE_IDLE
|
||||
|
||||
@ -159,6 +167,12 @@ class RadioThermostat(ClimateDevice):
|
||||
self.device.t_cool = round(temperature * 2.0) / 2.0
|
||||
elif self._current_operation == STATE_HEAT:
|
||||
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:
|
||||
self.device.hold = 1
|
||||
else:
|
||||
|
@ -16,6 +16,7 @@ from homeassistant.const import (
|
||||
ATTR_TEMPERATURE, CONF_API_KEY, CONF_ID, TEMP_CELSIUS, TEMP_FAHRENHEIT)
|
||||
from homeassistant.components.climate import (
|
||||
ATTR_CURRENT_HUMIDITY, ClimateDevice, PLATFORM_SCHEMA)
|
||||
from homeassistant.exceptions import PlatformNotReady
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
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)):
|
||||
if config[CONF_ID] == ALL or dev['id'] in config[CONF_ID]:
|
||||
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.')
|
||||
return False
|
||||
raise PlatformNotReady
|
||||
|
||||
if devices:
|
||||
async_add_devices(devices)
|
||||
|
@ -24,6 +24,17 @@ CONST_OVERLAY_MANUAL = 'MANUAL'
|
||||
# the temperature will be reset after a timespan
|
||||
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 = {
|
||||
CONST_OVERLAY_MANUAL: 'Manual',
|
||||
CONST_OVERLAY_TIMER: 'Timer',
|
||||
@ -60,9 +71,15 @@ def create_climate_device(tado, hass, zone, name, zone_id):
|
||||
capabilities = tado.get_capabilities(zone_id)
|
||||
|
||||
unit = TEMP_CELSIUS
|
||||
min_temp = float(capabilities['temperatures']['celsius']['min'])
|
||||
max_temp = float(capabilities['temperatures']['celsius']['max'])
|
||||
ac_mode = capabilities['type'] != 'HEATING'
|
||||
ac_mode = capabilities['type'] == 'AIR_CONDITIONING'
|
||||
|
||||
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)
|
||||
device = TadoClimate(tado,
|
||||
@ -107,7 +124,9 @@ class TadoClimate(ClimateDevice):
|
||||
self._max_temp = max_temp
|
||||
self._target_temp = None
|
||||
self._tolerance = tolerance
|
||||
self._cooling = False
|
||||
|
||||
self._current_fan = CONST_MODE_OFF
|
||||
self._current_operation = CONST_MODE_SMART_SCHEDULE
|
||||
self._overlay_mode = CONST_MODE_SMART_SCHEDULE
|
||||
|
||||
@ -129,13 +148,32 @@ class TadoClimate(ClimateDevice):
|
||||
@property
|
||||
def current_operation(self):
|
||||
"""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
|
||||
def operation_list(self):
|
||||
"""Return the list of available operation modes (readable)."""
|
||||
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
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement used by the platform."""
|
||||
@ -205,27 +243,27 @@ class TadoClimate(ClimateDevice):
|
||||
|
||||
if 'sensorDataPoints' in data:
|
||||
sensor_data = data['sensorDataPoints']
|
||||
temperature = float(
|
||||
sensor_data['insideTemperature']['celsius'])
|
||||
humidity = float(
|
||||
sensor_data['humidity']['percentage'])
|
||||
setting = 0
|
||||
|
||||
unit = TEMP_CELSIUS
|
||||
|
||||
if 'insideTemperature' in sensor_data:
|
||||
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
|
||||
if 'temperature' in data['setting'] and \
|
||||
data['setting']['temperature'] is not None:
|
||||
setting = float(
|
||||
data['setting']['temperature']['celsius'])
|
||||
|
||||
unit = TEMP_CELSIUS
|
||||
|
||||
self._cur_temp = self.hass.config.units.temperature(
|
||||
temperature, unit)
|
||||
|
||||
self._target_temp = self.hass.config.units.temperature(
|
||||
setting, unit)
|
||||
|
||||
self._cur_humidity = humidity
|
||||
self._target_temp = self.hass.config.units.temperature(
|
||||
setting, unit)
|
||||
|
||||
if 'tadoMode' in data:
|
||||
mode = data['tadoMode']
|
||||
@ -235,29 +273,39 @@ class TadoClimate(ClimateDevice):
|
||||
power = data['setting']['power']
|
||||
if power == '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
|
||||
else:
|
||||
self._device_is_active = True
|
||||
|
||||
if 'overlay' in data and data['overlay'] is not None:
|
||||
overlay = True
|
||||
termination = data['overlay']['termination']['type']
|
||||
else:
|
||||
if self._device_is_active:
|
||||
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
|
||||
# and a termination, but we want to see the mode "OFF"
|
||||
if 'overlay' in data:
|
||||
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._current_operation = termination
|
||||
else:
|
||||
# There is no overlay, the mode will always be
|
||||
# "SMART_SCHEDULE"
|
||||
self._overlay_mode = CONST_MODE_SMART_SCHEDULE
|
||||
self._current_operation = CONST_MODE_SMART_SCHEDULE
|
||||
self._cooling = cooling
|
||||
self._current_fan = fan_speed
|
||||
|
||||
def _control_heating(self):
|
||||
"""Send new target temperature to mytado."""
|
||||
|
134
homeassistant/components/comfoconnect.py
Normal file
134
homeassistant/components/comfoconnect.py
Normal 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)
|
@ -40,6 +40,8 @@ DEVICE_CLASSES = [
|
||||
'garage', # Garage door control
|
||||
]
|
||||
|
||||
DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.In(DEVICE_CLASSES))
|
||||
|
||||
SUPPORT_OPEN = 1
|
||||
SUPPORT_CLOSE = 2
|
||||
SUPPORT_SET_POSITION = 4
|
||||
|
185
homeassistant/components/cover/knx.py
Normal file
185
homeassistant/components/cover/knx.py
Normal 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
|
354
homeassistant/components/cover/template.py
Normal file
354
homeassistant/components/cover/template.py
Normal 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)
|
@ -210,6 +210,7 @@ def async_setup(hass, config):
|
||||
description=("Press the button on the bridge to register Philips "
|
||||
"Hue with Home Assistant."),
|
||||
description_image="/static/images/config_philips_hue.jpg",
|
||||
fields=[{'id': 'username', 'name': 'Username'}],
|
||||
submit_caption="I have pressed the button"
|
||||
)
|
||||
configurator_ids.append(request_id)
|
||||
|
110
homeassistant/components/device_tracker/linksys_smart.py
Normal file
110
homeassistant/components/device_tracker/linksys_smart.py
Normal 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)
|
@ -158,6 +158,11 @@ class MikrotikScanner(DeviceScanner):
|
||||
for device in devices
|
||||
}
|
||||
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
|
||||
|
@ -21,7 +21,7 @@ from homeassistant.components import zone as zone_comp
|
||||
from homeassistant.components.device_tracker import PLATFORM_SCHEMA
|
||||
|
||||
DEPENDENCIES = ['mqtt']
|
||||
REQUIREMENTS = ['libnacl==1.5.0']
|
||||
REQUIREMENTS = ['libnacl==1.5.1']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
32
homeassistant/components/device_tracker/ubus.py
Executable file → Normal file
32
homeassistant/components/device_tracker/ubus.py
Executable file → Normal file
@ -18,6 +18,7 @@ from homeassistant.components.device_tracker import (
|
||||
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.util import Throttle
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
|
||||
# Return cached results if last scan was less then this time ago.
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
|
||||
@ -38,6 +39,23 @@ def get_scanner(hass, config):
|
||||
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):
|
||||
"""
|
||||
This class queries a wireless router running OpenWrt firmware.
|
||||
@ -48,14 +66,16 @@ class UbusDeviceScanner(DeviceScanner):
|
||||
def __init__(self, config):
|
||||
"""Initialize the scanner."""
|
||||
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.lock = threading.Lock()
|
||||
self.last_results = {}
|
||||
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.leasefile = None
|
||||
self.mac2name = None
|
||||
@ -66,6 +86,7 @@ class UbusDeviceScanner(DeviceScanner):
|
||||
self._update_info()
|
||||
return self.last_results
|
||||
|
||||
@_refresh_on_acccess_denied
|
||||
def get_device_name(self, device):
|
||||
"""Return the name of the given device or None if we don't know."""
|
||||
with self.lock:
|
||||
@ -95,6 +116,7 @@ class UbusDeviceScanner(DeviceScanner):
|
||||
return self.mac2name.get(device.upper(), None)
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
||||
@_refresh_on_acccess_denied
|
||||
def _update_info(self):
|
||||
"""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:
|
||||
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":
|
||||
try:
|
||||
|
@ -13,7 +13,7 @@ from homeassistant.const import CONF_ACCESS_TOKEN
|
||||
from homeassistant.util import Throttle
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['python-digitalocean==1.11']
|
||||
REQUIREMENTS = ['python-digitalocean==1.12']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -29,7 +29,7 @@ ATTR_VCPUS = 'vcpus'
|
||||
|
||||
CONF_DROPLETS = 'droplets'
|
||||
|
||||
DIGITAL_OCEAN = None
|
||||
DATA_DIGITAL_OCEAN = 'data_do'
|
||||
DIGITAL_OCEAN_PLATFORMS = ['switch', 'binary_sensor']
|
||||
DOMAIN = 'digital_ocean'
|
||||
|
||||
@ -47,13 +47,14 @@ def setup(hass, config):
|
||||
conf = config[DOMAIN]
|
||||
access_token = conf.get(CONF_ACCESS_TOKEN)
|
||||
|
||||
global DIGITAL_OCEAN
|
||||
DIGITAL_OCEAN = DigitalOcean(access_token)
|
||||
digital = 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")
|
||||
return False
|
||||
|
||||
hass.data[DATA_DIGITAL_OCEAN] = digital
|
||||
|
||||
return True
|
||||
|
||||
|
||||
|
@ -55,6 +55,7 @@ SERVICE_HANDLERS = {
|
||||
'apple_tv': ('media_player', 'apple_tv'),
|
||||
'frontier_silicon': ('media_player', 'frontier_silicon'),
|
||||
'openhome': ('media_player', 'openhome'),
|
||||
'harmony': ('remote', 'harmony'),
|
||||
'bose_soundtouch': ('media_player', 'soundtouch'),
|
||||
}
|
||||
|
||||
|
118
homeassistant/components/fan/comfoconnect.py
Normal file
118
homeassistant/components/fan/comfoconnect.py
Normal 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()
|
195
homeassistant/components/fan/insteon_local.py
Normal file
195
homeassistant/components/fan/insteon_local.py
Normal 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
|
@ -3,21 +3,21 @@
|
||||
FINGERPRINTS = {
|
||||
"compatibility.js": "8e4c44b5f4288cc48ec1ba94a9bec812",
|
||||
"core.js": "d4a7cb8c80c62b536764e0e81385f6aa",
|
||||
"frontend.html": "cca45decbed803e7f0ec0b4f6e18fe53",
|
||||
"mdi.html": "1a5ad9654c1f0e57440e30afd92846a5",
|
||||
"frontend.html": "f170a7221615ca2839cb8fd51a82f50a",
|
||||
"mdi.html": "c92bd28c434865d6cabb34cd3c0a3e4c",
|
||||
"micromarkdown-js.html": "93b5ec4016f0bba585521cf4d18dec1a",
|
||||
"panels/ha-panel-automation.html": "21cba0a4fee9d2b45dda47f7a1dd82d8",
|
||||
"panels/ha-panel-config.html": "59d9eb28758b497a4d9b2428f978b9b1",
|
||||
"panels/ha-panel-dev-event.html": "2db9c218065ef0f61d8d08db8093cad2",
|
||||
"panels/ha-panel-dev-info.html": "61610e015a411cfc84edd2c4d489e71d",
|
||||
"panels/ha-panel-dev-service.html": "415552027cb083badeff5f16080410ed",
|
||||
"panels/ha-panel-dev-state.html": "d70314913b8923d750932367b1099750",
|
||||
"panels/ha-panel-dev-template.html": "567fbf86735e1b891e40c2f4060fec9b",
|
||||
"panels/ha-panel-automation.html": "4f98839bb082885657bbcd0ac04fc680",
|
||||
"panels/ha-panel-config.html": "76853de505d173e82249bf605eb73505",
|
||||
"panels/ha-panel-dev-event.html": "4886c821235492b1b92739b580d21c61",
|
||||
"panels/ha-panel-dev-info.html": "24e888ec7a8acd0c395b34396e9001bc",
|
||||
"panels/ha-panel-dev-service.html": "92c6be30b1af95791d5a6429df505852",
|
||||
"panels/ha-panel-dev-state.html": "8f1a27c04db6329d31cfcc7d0d6a0869",
|
||||
"panels/ha-panel-dev-template.html": "d33a55b937b50cdfe8b6fae81f70a139",
|
||||
"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-logbook.html": "6dd6a16f52117318b202e60f98400163",
|
||||
"panels/ha-panel-map.html": "31c592c239636f91e07c7ac232a5ebc4",
|
||||
"panels/ha-panel-zwave.html": "92edac58dd52c297c761fd9acec7f436",
|
||||
"panels/ha-panel-logbook.html": "7c45bd41c146ec38b9938b8a5188bb0d",
|
||||
"panels/ha-panel-map.html": "0ba605729197c4724ecc7310b08f7050",
|
||||
"panels/ha-panel-zwave.html": "2ea2223339d1d2faff478751c2927d11",
|
||||
"websocket_test.html": "575de64b431fe11c3785bf96d7813450"
|
||||
}
|
||||
|
File diff suppressed because one or more lines are too long
Binary file not shown.
@ -1 +1 @@
|
||||
Subproject commit 81ab4ff8a8ef7cc4b96b60f63c16472b0427adc7
|
||||
Subproject commit 1ad42592134c290119879e8f8505ef5736a3071e
|
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
@ -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>
|
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
@ -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
|
@ -51,7 +51,7 @@ def setup(hass, config):
|
||||
res = KNXTUNNEL.connect()
|
||||
_LOGGER.debug("Res = %s", 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
|
||||
|
||||
except KNXException as ex:
|
||||
@ -127,7 +127,10 @@ class KNXGroupAddress(Entity):
|
||||
self._config = config
|
||||
self._state = False
|
||||
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):
|
||||
"""Handle an incoming KNX frame.
|
||||
@ -198,11 +201,15 @@ class KNXGroupAddress(Entity):
|
||||
self._data = res
|
||||
else:
|
||||
_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:
|
||||
_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
|
||||
|
||||
|
||||
@ -213,9 +220,6 @@ class KNXMultiAddressDevice(Entity):
|
||||
to be controlled by multiple group addresses.
|
||||
"""
|
||||
|
||||
names = {}
|
||||
values = {}
|
||||
|
||||
def __init__(self, hass, config, required, optional=None):
|
||||
"""Initialize the device.
|
||||
|
||||
@ -226,33 +230,69 @@ class KNXMultiAddressDevice(Entity):
|
||||
"""
|
||||
from knxip.core import parse_group_address, KNXException
|
||||
|
||||
self.names = {}
|
||||
self.values = {}
|
||||
|
||||
self._config = config
|
||||
self._state = False
|
||||
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
|
||||
for name in required:
|
||||
_LOGGER.info(name)
|
||||
paramname = '{}{}'.format(name, '_address')
|
||||
addr = self._config.config.get(paramname)
|
||||
addr = settings.get(paramname)
|
||||
if addr is None:
|
||||
_LOGGER.exception(
|
||||
"Required KNX group address %s missing", paramname)
|
||||
_LOGGER.error(
|
||||
"%s: Required KNX group address %s missing",
|
||||
self.name, paramname
|
||||
)
|
||||
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)
|
||||
self.names[addr] = name
|
||||
|
||||
# parse optional addresses
|
||||
for name in optional:
|
||||
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:
|
||||
try:
|
||||
addr = parse_group_address(addr)
|
||||
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
|
||||
|
||||
@property
|
||||
@ -280,11 +320,53 @@ class KNXMultiAddressDevice(Entity):
|
||||
|
||||
This is mostly important for optional addresses.
|
||||
"""
|
||||
for attributename, dummy_attribute in self.names.items():
|
||||
for attributename in self.names.values():
|
||||
if attributename == name:
|
||||
return True
|
||||
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):
|
||||
"""Return the value to a given named attribute."""
|
||||
from knxip.core import KNXException
|
||||
@ -295,13 +377,21 @@ class KNXMultiAddressDevice(Entity):
|
||||
addr = attributeaddress
|
||||
|
||||
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
|
||||
|
||||
try:
|
||||
res = KNXTUNNEL.group_read(addr, use_cache=self.cache)
|
||||
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 res
|
||||
@ -316,13 +406,21 @@ class KNXMultiAddressDevice(Entity):
|
||||
addr = attributeaddress
|
||||
|
||||
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
|
||||
|
||||
try:
|
||||
KNXTUNNEL.group_write(addr, value)
|
||||
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 True
|
||||
|
@ -14,7 +14,7 @@ from homeassistant.components.light import (
|
||||
PLATFORM_SCHEMA)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['decora==0.4']
|
||||
REQUIREMENTS = ['decora==0.6']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -59,7 +59,7 @@ class DecoraLight(Light):
|
||||
self._switch = decora.decora(self._address, self._key)
|
||||
self._switch.connect()
|
||||
self._state = self._switch.get_on()
|
||||
self._brightness = self._switch.get_brightness()
|
||||
self._brightness = self._switch.get_brightness() * 2.55
|
||||
self.is_valid = True
|
||||
|
||||
@property
|
||||
@ -99,7 +99,7 @@ class DecoraLight(Light):
|
||||
|
||||
def set_state(self, 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
|
||||
return True
|
||||
|
||||
@ -120,5 +120,5 @@ class DecoraLight(Light):
|
||||
|
||||
def update(self):
|
||||
"""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()
|
||||
|
@ -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')
|
@ -56,7 +56,7 @@ class ISYLightDevice(isy.ISYDevice, Light):
|
||||
def turn_off(self, **kwargs) -> None:
|
||||
"""Send the turn off command to the ISY994 light device."""
|
||||
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:
|
||||
"""Send the turn on command to the ISY994 light device."""
|
||||
|
@ -11,18 +11,19 @@ import math
|
||||
from os import path
|
||||
from functools import partial
|
||||
from datetime import timedelta
|
||||
import async_timeout
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.light import (
|
||||
Light, DOMAIN, PLATFORM_SCHEMA, LIGHT_TURN_ON_SCHEMA,
|
||||
ATTR_BRIGHTNESS, ATTR_RGB_COLOR,
|
||||
ATTR_XY_COLOR, ATTR_COLOR_TEMP, ATTR_TRANSITION, ATTR_EFFECT,
|
||||
ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, ATTR_COLOR_NAME, ATTR_RGB_COLOR,
|
||||
ATTR_XY_COLOR, ATTR_COLOR_TEMP, ATTR_KELVIN, ATTR_TRANSITION, ATTR_EFFECT,
|
||||
SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_RGB_COLOR,
|
||||
SUPPORT_XY_COLOR, SUPPORT_TRANSITION, SUPPORT_EFFECT,
|
||||
VALID_BRIGHTNESS, VALID_BRIGHTNESS_PCT,
|
||||
preprocess_turn_on_alternatives)
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.const import ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant import util
|
||||
from homeassistant.core import callback
|
||||
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.util.color as color_util
|
||||
|
||||
from . import effects as lifx_effects
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
REQUIREMENTS = ['aiolifx==0.4.8']
|
||||
REQUIREMENTS = ['aiolifx==0.5.0', 'aiolifx_effects==0.1.0']
|
||||
|
||||
UDP_BROADCAST_PORT = 56700
|
||||
|
||||
# Delay (in ms) expected for changes to take effect in the physical bulb
|
||||
BULB_LATENCY = 500
|
||||
|
||||
CONF_SERVER = 'server'
|
||||
|
||||
SERVICE_LIFX_SET_STATE = 'lifx_set_state'
|
||||
|
||||
ATTR_HSBK = 'hsbk'
|
||||
ATTR_INFRARED = 'infrared'
|
||||
ATTR_POWER = 'power'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
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({
|
||||
ATTR_INFRARED: vol.All(vol.Coerce(int), vol.Clamp(min=0, max=255)),
|
||||
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
|
||||
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)
|
||||
|
||||
lifx_manager = LIFXManager(hass, async_add_devices)
|
||||
lifx_discovery = aiolifx.LifxDiscovery(hass.loop, lifx_manager)
|
||||
|
||||
coro = hass.loop.create_datagram_endpoint(
|
||||
partial(aiolifx.LifxDiscovery, hass.loop, lifx_manager),
|
||||
local_addr=(server_addr, UDP_BROADCAST_PORT))
|
||||
lambda: lifx_discovery, local_addr=(server_addr, UDP_BROADCAST_PORT))
|
||||
|
||||
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
|
||||
|
||||
|
||||
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):
|
||||
"""Representation of all known LIFX entities."""
|
||||
|
||||
def __init__(self, hass, async_add_devices):
|
||||
"""Initialize the light."""
|
||||
import aiolifx_effects
|
||||
self.entities = {}
|
||||
self.hass = hass
|
||||
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
|
||||
def async_service_handle(service):
|
||||
"""Apply a service."""
|
||||
@ -99,22 +197,73 @@ class LIFXManager(object):
|
||||
for light in self.service_to_entities(service):
|
||||
if service.service == SERVICE_LIFX_SET_STATE:
|
||||
task = light.async_set_state(**service.data)
|
||||
tasks.append(hass.async_add_job(task))
|
||||
tasks.append(self.hass.async_add_job(task))
|
||||
if tasks:
|
||||
yield from asyncio.wait(tasks, loop=hass.loop)
|
||||
yield from asyncio.wait(tasks, loop=self.hass.loop)
|
||||
|
||||
descriptions = self.get_descriptions()
|
||||
|
||||
hass.services.async_register(
|
||||
self.hass.services.async_register(
|
||||
DOMAIN, SERVICE_LIFX_SET_STATE, async_service_handle,
|
||||
descriptions.get(SERVICE_LIFX_SET_STATE),
|
||||
schema=LIFX_SET_STATE_SCHEMA)
|
||||
|
||||
@staticmethod
|
||||
def get_descriptions():
|
||||
"""Load and return descriptions for our own service calls."""
|
||||
return load_yaml_config_file(
|
||||
path.join(path.dirname(__file__), 'services.yaml'))
|
||||
def register_effects(self, descriptions):
|
||||
"""Register the LIFX effects as hass service calls."""
|
||||
@asyncio.coroutine
|
||||
def async_service_handle(service):
|
||||
"""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):
|
||||
"""Return the known devices that a service call mentions."""
|
||||
@ -148,7 +297,7 @@ class LIFXManager(object):
|
||||
@callback
|
||||
def ready(self, device, msg):
|
||||
"""Handle the device once all data is retrieved."""
|
||||
entity = LIFXLight(device)
|
||||
entity = LIFXLight(device, self.effects_conductor)
|
||||
_LOGGER.debug("%s register READY", entity.who)
|
||||
self.entities[device.mac_addr] = entity
|
||||
self.async_add_devices([entity])
|
||||
@ -182,17 +331,13 @@ class AwaitAioLIFX:
|
||||
|
||||
@asyncio.coroutine
|
||||
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()
|
||||
method(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
|
||||
method(callb=self.callback)
|
||||
|
||||
yield from self.event.wait()
|
||||
return self.message
|
||||
|
||||
|
||||
@ -209,17 +354,13 @@ def convert_16_to_8(value):
|
||||
class LIFXLight(Light):
|
||||
"""Representation of a LIFX light."""
|
||||
|
||||
def __init__(self, device):
|
||||
def __init__(self, device, effects_conductor):
|
||||
"""Initialize the light."""
|
||||
self.device = device
|
||||
self.effects_conductor = effects_conductor
|
||||
self.registered = True
|
||||
self.product = device.product
|
||||
self.blocker = None
|
||||
self.effect_data = None
|
||||
self.postponed_update = None
|
||||
self._name = device.label
|
||||
self.set_power(device.power_level)
|
||||
self.set_color(*device.color)
|
||||
|
||||
@property
|
||||
def lifxwhite(self):
|
||||
@ -235,34 +376,33 @@ class LIFXLight(Light):
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
return self._name
|
||||
return self.device.label
|
||||
|
||||
@property
|
||||
def who(self):
|
||||
"""Return a string identifying the device."""
|
||||
ip_addr = '-'
|
||||
if self.device:
|
||||
ip_addr = self.device.ip_addr[0]
|
||||
return "%s (%s)" % (ip_addr, self.name)
|
||||
return "%s (%s)" % (self.device.ip_addr, self.name)
|
||||
|
||||
@property
|
||||
def rgb_color(self):
|
||||
"""Return the RGB value."""
|
||||
_LOGGER.debug(
|
||||
"rgb_color: [%d %d %d]", self._rgb[0], self._rgb[1], self._rgb[2])
|
||||
return self._rgb
|
||||
hue, sat, bri, _ = self.device.color
|
||||
|
||||
return color_util.color_hsv_to_RGB(
|
||||
hue, convert_16_to_8(sat), convert_16_to_8(bri))
|
||||
|
||||
@property
|
||||
def brightness(self):
|
||||
"""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)
|
||||
return brightness
|
||||
|
||||
@property
|
||||
def color_temp(self):
|
||||
"""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)
|
||||
return temperature
|
||||
@ -290,13 +430,15 @@ class LIFXLight(Light):
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if device is on."""
|
||||
_LOGGER.debug("is_on: %d", self._power)
|
||||
return self._power != 0
|
||||
return self.device.power_level != 0
|
||||
|
||||
@property
|
||||
def effect(self):
|
||||
"""Return the currently running effect."""
|
||||
return self.effect_data.effect.name if self.effect_data else None
|
||||
"""Return the name of the currently running effect."""
|
||||
effect = self.effects_conductor.effect(self.device)
|
||||
if effect:
|
||||
return 'lifx_effect_' + effect.name
|
||||
return None
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
@ -311,38 +453,35 @@ class LIFXLight(Light):
|
||||
|
||||
@property
|
||||
def effect_list(self):
|
||||
"""Return the list of supported effects."""
|
||||
return lifx_effects.effect_list(self)
|
||||
"""Return the list of supported effects for this light."""
|
||||
if self.lifxwhite:
|
||||
return [
|
||||
SERVICE_EFFECT_PULSE,
|
||||
SERVICE_EFFECT_STOP,
|
||||
]
|
||||
|
||||
return [
|
||||
SERVICE_EFFECT_COLORLOOP,
|
||||
SERVICE_EFFECT_PULSE,
|
||||
SERVICE_EFFECT_STOP,
|
||||
]
|
||||
|
||||
@asyncio.coroutine
|
||||
def update_after_transition(self, now):
|
||||
"""Request new status after completion of the last transition."""
|
||||
self.postponed_update = None
|
||||
yield from self.refresh_state()
|
||||
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()
|
||||
yield from self.async_update_ha_state()
|
||||
|
||||
def update_later(self, when):
|
||||
"""Block immediate update requests and schedule one for later."""
|
||||
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))
|
||||
|
||||
"""Schedule an update requests when a transition is over."""
|
||||
if self.postponed_update:
|
||||
self.postponed_update()
|
||||
self.postponed_update = None
|
||||
if when > BULB_LATENCY:
|
||||
if when > 0:
|
||||
self.postponed_update = async_track_point_in_utc_time(
|
||||
self.hass, self.update_after_transition,
|
||||
util.dt.utcnow() + timedelta(milliseconds=when+BULB_LATENCY))
|
||||
util.dt.utcnow() + timedelta(milliseconds=when))
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_turn_on(self, **kwargs):
|
||||
@ -359,10 +498,10 @@ class LIFXLight(Light):
|
||||
@asyncio.coroutine
|
||||
def async_set_state(self, **kwargs):
|
||||
"""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:
|
||||
yield from lifx_effects.default_effect(self, **kwargs)
|
||||
yield from self.default_effect(**kwargs)
|
||||
return
|
||||
|
||||
if ATTR_INFRARED in kwargs:
|
||||
@ -377,124 +516,44 @@ class LIFXLight(Light):
|
||||
power_on = kwargs.get(ATTR_POWER, False)
|
||||
power_off = not kwargs.get(ATTR_POWER, True)
|
||||
|
||||
hsbk, changed_color = self.find_hsbk(**kwargs)
|
||||
_LOGGER.debug("turn_on: %s (%d) %d %d %d %d %d",
|
||||
self.who, self._power, fade, *hsbk)
|
||||
hsbk = merge_hsbk(self.device.color, find_hsbk(**kwargs))
|
||||
|
||||
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:
|
||||
self.device.set_power(False, None, 0)
|
||||
if changed_color:
|
||||
self.device.set_color(hsbk, None, 0)
|
||||
yield from ack(partial(bulb.set_power, False))
|
||||
if hsbk:
|
||||
yield from ack(partial(bulb.set_color, hsbk))
|
||||
if power_on:
|
||||
self.device.set_power(True, None, fade)
|
||||
yield from ack(partial(bulb.set_power, True, duration=fade))
|
||||
else:
|
||||
if power_on:
|
||||
self.device.set_power(True, None, 0)
|
||||
if changed_color:
|
||||
self.device.set_color(hsbk, None, fade)
|
||||
yield from ack(partial(bulb.set_power, True))
|
||||
if hsbk:
|
||||
yield from ack(partial(bulb.set_color, hsbk, duration=fade))
|
||||
if power_off:
|
||||
self.device.set_power(False, None, fade)
|
||||
yield from ack(partial(bulb.set_power, False, duration=fade))
|
||||
|
||||
if power_on:
|
||||
self.update_later(0)
|
||||
else:
|
||||
self.update_later(fade)
|
||||
# Schedule an update when the transition is complete
|
||||
self.update_later(fade)
|
||||
|
||||
if fade <= BULB_LATENCY:
|
||||
if power_on:
|
||||
self.set_power(1)
|
||||
if power_off:
|
||||
self.set_power(0)
|
||||
if changed_color:
|
||||
self.set_color(*hsbk)
|
||||
@asyncio.coroutine
|
||||
def default_effect(self, **kwargs):
|
||||
"""Start an effect with default parameters."""
|
||||
service = kwargs[ATTR_EFFECT]
|
||||
data = {
|
||||
ATTR_ENTITY_ID: self.entity_id,
|
||||
}
|
||||
yield from self.hass.services.async_call(DOMAIN, service, data)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_update(self):
|
||||
"""Update bulb status (if it is available)."""
|
||||
"""Update bulb status."""
|
||||
_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:
|
||||
msg = yield from AwaitAioLIFX(self).wait(self.device.get_color)
|
||||
if msg is not None:
|
||||
self.set_power(self.device.power_level)
|
||||
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]
|
||||
# Avoid state ping-pong by holding off updates as the state settles
|
||||
yield from asyncio.sleep(0.25)
|
||||
yield from AwaitAioLIFX(self).wait(self.device.get_color)
|
@ -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
|
@ -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'
|
@ -24,11 +24,13 @@ CONF_BRIDGES = 'bridges'
|
||||
CONF_GROUPS = 'groups'
|
||||
CONF_NUMBER = 'number'
|
||||
CONF_VERSION = 'version'
|
||||
CONF_FADE = 'fade'
|
||||
|
||||
DEFAULT_LED_TYPE = 'rgbw'
|
||||
DEFAULT_PORT = 5987
|
||||
DEFAULT_TRANSITION = 0
|
||||
DEFAULT_VERSION = 6
|
||||
DEFAULT_FADE = False
|
||||
|
||||
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.In(LED_TYPE),
|
||||
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_NAME),
|
||||
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)
|
||||
|
||||
|
||||
@ -152,25 +157,26 @@ def state(new_state):
|
||||
class LimitlessLEDGroup(Light):
|
||||
"""Representation of a LimitessLED group."""
|
||||
|
||||
def __init__(self, group):
|
||||
def __init__(self, group, config):
|
||||
"""Initialize a group."""
|
||||
self.group = group
|
||||
self.repeating = False
|
||||
self._is_on = False
|
||||
self._brightness = None
|
||||
self.config = config
|
||||
|
||||
@staticmethod
|
||||
def factory(group):
|
||||
def factory(group, config):
|
||||
"""Produce LimitlessLEDGroup objects."""
|
||||
from limitlessled.group.rgbw import RgbwGroup
|
||||
from limitlessled.group.white import WhiteGroup
|
||||
from limitlessled.group.rgbww import RgbwwGroup
|
||||
if isinstance(group, WhiteGroup):
|
||||
return LimitlessLEDWhiteGroup(group)
|
||||
return LimitlessLEDWhiteGroup(group, config)
|
||||
elif isinstance(group, RgbwGroup):
|
||||
return LimitlessLEDRGBWGroup(group)
|
||||
return LimitlessLEDRGBWGroup(group, config)
|
||||
elif isinstance(group, RgbwwGroup):
|
||||
return LimitlessLEDRGBWWGroup(group)
|
||||
return LimitlessLEDRGBWWGroup(group, config)
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
@ -196,15 +202,17 @@ class LimitlessLEDGroup(Light):
|
||||
def turn_off(self, transition_time, pipeline, **kwargs):
|
||||
"""Turn off a group."""
|
||||
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):
|
||||
"""Representation of a LimitlessLED White group."""
|
||||
|
||||
def __init__(self, group):
|
||||
def __init__(self, group, config):
|
||||
"""Initialize White group."""
|
||||
super().__init__(group)
|
||||
super().__init__(group, config)
|
||||
# Initialize group with known values.
|
||||
self.group.on = True
|
||||
self.group.temperature = 1.0
|
||||
@ -242,9 +250,9 @@ class LimitlessLEDWhiteGroup(LimitlessLEDGroup):
|
||||
class LimitlessLEDRGBWGroup(LimitlessLEDGroup):
|
||||
"""Representation of a LimitlessLED RGBW group."""
|
||||
|
||||
def __init__(self, group):
|
||||
def __init__(self, group, config):
|
||||
"""Initialize RGBW group."""
|
||||
super().__init__(group)
|
||||
super().__init__(group, config)
|
||||
# Initialize group with known values.
|
||||
self.group.on = True
|
||||
self.group.white()
|
||||
@ -301,9 +309,9 @@ class LimitlessLEDRGBWGroup(LimitlessLEDGroup):
|
||||
class LimitlessLEDRGBWWGroup(LimitlessLEDGroup):
|
||||
"""Representation of a LimitlessLED RGBWW group."""
|
||||
|
||||
def __init__(self, group):
|
||||
def __init__(self, group, config):
|
||||
"""Initialize RGBWW group."""
|
||||
super().__init__(group)
|
||||
super().__init__(group, config)
|
||||
# Initialize group with known values.
|
||||
self.group.on = True
|
||||
self.group.white()
|
||||
|
@ -101,3 +101,98 @@ hue_activate_scene:
|
||||
scene_name:
|
||||
description: Name of hue scene from the hue app
|
||||
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'
|
||||
|
@ -5,7 +5,8 @@ For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/verisure/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from time import sleep
|
||||
from time import time
|
||||
from homeassistant.components.verisure import HUB as hub
|
||||
from homeassistant.components.verisure import (CONF_LOCKS, CONF_CODE_DIGITS)
|
||||
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."""
|
||||
locks = []
|
||||
if int(hub.config.get(CONF_LOCKS, 1)):
|
||||
hub.update_locks()
|
||||
hub.update_overview()
|
||||
locks.extend([
|
||||
VerisureDoorlock(device_id)
|
||||
for device_id in hub.lock_status
|
||||
])
|
||||
VerisureDoorlock(device_label)
|
||||
for device_label in hub.get(
|
||||
"$.doorLockStatusList[*].deviceLabel")])
|
||||
|
||||
add_devices(locks)
|
||||
|
||||
|
||||
class VerisureDoorlock(LockDevice):
|
||||
"""Representation of a Verisure doorlock."""
|
||||
|
||||
def __init__(self, device_id):
|
||||
def __init__(self, device_label):
|
||||
"""Initialize the Verisure lock."""
|
||||
self._id = device_id
|
||||
self._device_label = device_label
|
||||
self._state = STATE_UNKNOWN
|
||||
self._digits = hub.config.get(CONF_CODE_DIGITS)
|
||||
self._changed_by = None
|
||||
self._change_timestamp = 0
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""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
|
||||
def state(self):
|
||||
@ -50,7 +55,9 @@ class VerisureDoorlock(LockDevice):
|
||||
@property
|
||||
def available(self):
|
||||
"""Return True if entity is available."""
|
||||
return hub.available
|
||||
return hub.get_first(
|
||||
"$.doorLockStatusList[?(@.deviceLabel=='%s')]",
|
||||
self._device_label) is not None
|
||||
|
||||
@property
|
||||
def changed_by(self):
|
||||
@ -64,32 +71,52 @@ class VerisureDoorlock(LockDevice):
|
||||
|
||||
def update(self):
|
||||
"""Update lock status."""
|
||||
hub.update_locks()
|
||||
|
||||
if hub.lock_status[self._id].status == 'unlocked':
|
||||
if time() - self._change_timestamp < 10:
|
||||
return
|
||||
hub.update_overview()
|
||||
status = hub.get_first(
|
||||
"$.doorLockStatusList[?(@.deviceLabel=='%s')].lockedState",
|
||||
self._device_label)
|
||||
if status == 'UNLOCKED':
|
||||
self._state = STATE_UNLOCKED
|
||||
elif hub.lock_status[self._id].status == 'locked':
|
||||
elif status == 'LOCKED':
|
||||
self._state = STATE_LOCKED
|
||||
elif hub.lock_status[self._id].status != 'pending':
|
||||
_LOGGER.error(
|
||||
"Unknown lock state %s", hub.lock_status[self._id].status)
|
||||
self._changed_by = hub.lock_status[self._id].name
|
||||
elif status != 'PENDING':
|
||||
_LOGGER.error('Unknown lock state %s', status)
|
||||
self._changed_by = hub.get_first(
|
||||
"$.doorLockStatusList[?(@.deviceLabel=='%s')].userString",
|
||||
self._device_label)
|
||||
|
||||
@property
|
||||
def is_locked(self):
|
||||
"""Return true if lock is locked."""
|
||||
return hub.lock_status[self._id].status
|
||||
return self._state == STATE_LOCKED
|
||||
|
||||
def unlock(self, **kwargs):
|
||||
"""Send unlock command."""
|
||||
hub.my_pages.lock.set(kwargs[ATTR_CODE], self._id, 'UNLOCKED')
|
||||
_LOGGER.debug("Verisure doorlock unlocking")
|
||||
hub.my_pages.lock.wait_while_pending()
|
||||
self.update()
|
||||
if self._state == STATE_UNLOCKED:
|
||||
return
|
||||
self.set_lock_state(kwargs[ATTR_CODE], STATE_UNLOCKED)
|
||||
|
||||
def lock(self, **kwargs):
|
||||
"""Send lock command."""
|
||||
hub.my_pages.lock.set(kwargs[ATTR_CODE], self._id, 'LOCKED')
|
||||
_LOGGER.debug("Verisure doorlock locking")
|
||||
hub.my_pages.lock.wait_while_pending()
|
||||
self.update()
|
||||
if self._state == STATE_LOCKED:
|
||||
return
|
||||
self.set_lock_state(kwargs[ATTR_CODE], STATE_LOCKED)
|
||||
|
||||
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()
|
||||
|
@ -6,6 +6,7 @@ https://home-assistant.io/components/media_player.denon/
|
||||
"""
|
||||
|
||||
import logging
|
||||
from collections import namedtuple
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.media_player import (
|
||||
@ -16,16 +17,19 @@ from homeassistant.components.media_player import (
|
||||
MEDIA_TYPE_MUSIC, SUPPORT_VOLUME_SET, SUPPORT_PLAY)
|
||||
from homeassistant.const import (
|
||||
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
|
||||
|
||||
REQUIREMENTS = ['denonavr==0.4.4']
|
||||
REQUIREMENTS = ['denonavr==0.5.1']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_NAME = None
|
||||
DEFAULT_SHOW_SOURCES = False
|
||||
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'
|
||||
|
||||
SUPPORT_DENON = SUPPORT_VOLUME_STEP | SUPPORT_VOLUME_MUTE | \
|
||||
@ -36,16 +40,26 @@ SUPPORT_MEDIA_MODES = SUPPORT_PLAY_MEDIA | \
|
||||
SUPPORT_PAUSE | SUPPORT_PREVIOUS_TRACK | \
|
||||
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({
|
||||
vol.Optional(CONF_HOST): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_SHOW_ALL_SOURCES, default=DEFAULT_SHOW_SOURCES):
|
||||
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):
|
||||
"""Set up the Denon platform."""
|
||||
# pylint: disable=import-error
|
||||
import denonavr
|
||||
|
||||
# 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:
|
||||
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)
|
||||
|
||||
# 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
|
||||
if config.get(CONF_HOST) is not None:
|
||||
host = config.get(CONF_HOST)
|
||||
name = config.get(CONF_NAME)
|
||||
# Check if host not in cache, append it and save for later starting
|
||||
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)
|
||||
new_hosts.append(NewHost(host=host, name=name))
|
||||
|
||||
# 2. option: discovery using netdisco
|
||||
if discovery_info is not None:
|
||||
host = discovery_info.get('host')
|
||||
name = discovery_info.get('name')
|
||||
# Check if host not in cache, append it and save for later starting
|
||||
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)
|
||||
new_hosts.append(NewHost(host=host, name=name))
|
||||
|
||||
# 3. option: discovery using denonavr library
|
||||
if config.get(CONF_HOST) is None and discovery_info is None:
|
||||
d_receivers = denonavr.discover()
|
||||
@ -85,14 +103,18 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
for d_receiver in d_receivers:
|
||||
host = d_receiver["host"]
|
||||
name = d_receiver["friendlyName"]
|
||||
# Check if host not in cache, append it and save for later
|
||||
# starting
|
||||
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)
|
||||
new_hosts.append(NewHost(host=host, name=name))
|
||||
|
||||
for entry in new_hosts:
|
||||
# Check if host not in cache, append it and save for later
|
||||
# starting
|
||||
if entry.host not in cache:
|
||||
new_device = denonavr.DenonAVR(
|
||||
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
|
||||
if receivers:
|
||||
|
@ -21,7 +21,7 @@ from homeassistant.core import callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
REQUIREMENTS = ['pyemby==1.2']
|
||||
REQUIREMENTS = ['pyemby==1.3']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -540,7 +540,7 @@ class KodiDevice(MediaPlayerDevice):
|
||||
elif self._turn_off_action == 'shutdown':
|
||||
yield from self.server.System.Shutdown()
|
||||
else:
|
||||
_LOGGER.warning('turn_off requested but turn_off_action is none')
|
||||
_LOGGER.warning("turn_off requested but turn_off_action is none")
|
||||
|
||||
@cmd
|
||||
@asyncio.coroutine
|
||||
@ -694,22 +694,26 @@ class KodiDevice(MediaPlayerDevice):
|
||||
def async_call_method(self, method, **kwargs):
|
||||
"""Run Kodi JSONRPC API method with params."""
|
||||
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
|
||||
try:
|
||||
result = yield from getattr(self.server, method)(**kwargs)
|
||||
result_ok = True
|
||||
except jsonrpc_base.jsonrpc.ProtocolError as exc:
|
||||
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)
|
||||
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):
|
||||
event_data = {'entity_id': self.entity_id,
|
||||
'result': result,
|
||||
'result_ok': result_ok,
|
||||
'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,
|
||||
event_data=event_data)
|
||||
return result
|
||||
@ -753,10 +757,13 @@ class KodiDevice(MediaPlayerDevice):
|
||||
yield from self.server.Playlist.Add(params)
|
||||
except jsonrpc_base.jsonrpc.ProtocolError as exc:
|
||||
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)
|
||||
except jsonrpc_base.jsonrpc.TransportError:
|
||||
_LOGGER.warning("TransportError trying to add playlist to %s",
|
||||
self.entity_id)
|
||||
else:
|
||||
_LOGGER.warning('No media detected for Playlist.Add')
|
||||
_LOGGER.warning("No media detected for Playlist.Add")
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_add_all_albums(self, artist_name):
|
||||
@ -800,7 +807,7 @@ class KodiDevice(MediaPlayerDevice):
|
||||
artist_name, [a['artist'] for a in artists['artists']])
|
||||
return artists['artists'][out[0][0]]['artistid']
|
||||
except KeyError:
|
||||
_LOGGER.warning('No artists were found: %s', artist_name)
|
||||
_LOGGER.warning("No artists were found: %s", artist_name)
|
||||
return None
|
||||
|
||||
@asyncio.coroutine
|
||||
@ -839,7 +846,7 @@ class KodiDevice(MediaPlayerDevice):
|
||||
album_name, [a['label'] for a in albums['albums']])
|
||||
return albums['albums'][out[0][0]]['albumid']
|
||||
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)
|
||||
return None
|
||||
|
||||
|
@ -14,7 +14,8 @@ from homeassistant.components.media_player import (
|
||||
MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, PLATFORM_SCHEMA,
|
||||
SUPPORT_PREVIOUS_TRACK, SUPPORT_TURN_OFF, SUPPORT_TURN_ON,
|
||||
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 (
|
||||
STATE_OFF, STATE_PAUSED, STATE_PLAYING, CONF_PORT, CONF_PASSWORD,
|
||||
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_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({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
@ -266,3 +268,20 @@ class MpdDevice(MediaPlayerDevice):
|
||||
self.client.clear()
|
||||
self.client.add(media_id)
|
||||
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)
|
||||
|
@ -88,6 +88,8 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
elif discovery_info is not None:
|
||||
# Parse discovery data
|
||||
host = discovery_info.get('host')
|
||||
port = discovery_info.get('port')
|
||||
host = '%s:%s' % (host, port)
|
||||
_LOGGER.info("Discovered PLEX server: %s", host)
|
||||
|
||||
if host in _CONFIGURING:
|
||||
@ -106,6 +108,7 @@ def setup_plexserver(host, token, hass, config, add_devices_callback):
|
||||
|
||||
try:
|
||||
plexserver = plexapi.server.PlexServer('http://%s' % host, token)
|
||||
_LOGGER.info("Discovery configuration done (no token needed)")
|
||||
except (plexapi.exceptions.BadRequest, plexapi.exceptions.Unauthorized,
|
||||
plexapi.exceptions.NotFound) as error:
|
||||
_LOGGER.info(error)
|
||||
|
@ -20,7 +20,7 @@ from homeassistant.const import (CONF_HOST, CONF_NAME, STATE_OFF, CONF_PORT,
|
||||
STATE_PAUSED, STATE_PLAYING,
|
||||
STATE_UNAVAILABLE)
|
||||
|
||||
REQUIREMENTS = ['libsoundtouch==0.3.0']
|
||||
REQUIREMENTS = ['libsoundtouch==0.6.2']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -29,7 +29,7 @@ from homeassistant.const import (
|
||||
CONF_PASSWORD, CONF_PORT, CONF_PROTOCOL, CONF_PAYLOAD)
|
||||
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__)
|
||||
|
||||
|
@ -17,8 +17,8 @@ from homeassistant.components.mqtt import CONF_STATE_TOPIC
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
TOPIC_MATCHER = re.compile(
|
||||
r'(?P<prefix_topic>\w+)/(?P<component>\w+)/(?P<object_id>[a-zA-Z0-9_-]+)'
|
||||
'/config')
|
||||
r'(?P<prefix_topic>\w+)/(?P<component>\w+)/'
|
||||
r'(?:(?P<node_id>[a-zA-Z0-9_-]+)/)?(?P<object_id>[a-zA-Z0-9_-]+)/config')
|
||||
|
||||
SUPPORTED_COMPONENTS = ['binary_sensor', 'light', 'sensor', 'switch']
|
||||
|
||||
@ -44,7 +44,7 @@ def async_start(hass, discovery_topic, hass_config):
|
||||
if not match:
|
||||
return
|
||||
|
||||
prefix_topic, component, object_id = match.groups()
|
||||
prefix_topic, component, node_id, object_id = match.groups()
|
||||
|
||||
try:
|
||||
payload = json.loads(payload)
|
||||
@ -65,21 +65,25 @@ def async_start(hass, discovery_topic, hass_config):
|
||||
|
||||
payload[CONF_PLATFORM] = platform
|
||||
if CONF_STATE_TOPIC not in payload:
|
||||
payload[CONF_STATE_TOPIC] = '{}/{}/{}/state'.format(
|
||||
discovery_topic, component, object_id)
|
||||
payload[CONF_STATE_TOPIC] = '{}/{}/{}{}/state'.format(
|
||||
discovery_topic, component, '%s/' % node_id if node_id else '',
|
||||
object_id)
|
||||
|
||||
if ALREADY_DISCOVERED not in hass.data:
|
||||
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]:
|
||||
_LOGGER.info("Component has already been discovered: %s %s",
|
||||
component, object_id)
|
||||
component, discovery_id)
|
||||
return
|
||||
|
||||
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(
|
||||
hass, component, platform, payload, hass_config)
|
||||
|
80
homeassistant/components/notify/clicksend.py
Normal file
80
homeassistant/components/notify/clicksend.py
Normal 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
|
@ -25,7 +25,7 @@ from homeassistant.components.http import HomeAssistantView
|
||||
from homeassistant.components.frontend import add_manifest_json_key
|
||||
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']
|
||||
|
||||
|
@ -29,16 +29,18 @@ _LOGGER = logging.getLogger(__name__)
|
||||
ATTR_IMAGES = 'images' # optional embedded image file attachments
|
||||
ATTR_HTML = 'html'
|
||||
|
||||
CONF_STARTTLS = 'starttls'
|
||||
CONF_ENCRYPTION = 'encryption'
|
||||
CONF_DEBUG = 'debug'
|
||||
CONF_SERVER = 'server'
|
||||
CONF_SENDER_NAME = 'sender_name'
|
||||
|
||||
DEFAULT_HOST = 'localhost'
|
||||
DEFAULT_PORT = 25
|
||||
DEFAULT_PORT = 587
|
||||
DEFAULT_TIMEOUT = 5
|
||||
DEFAULT_DEBUG = False
|
||||
DEFAULT_STARTTLS = False
|
||||
DEFAULT_ENCRYPTION = 'starttls'
|
||||
|
||||
ENCRYPTION_OPTIONS = ['tls', 'starttls', 'none']
|
||||
|
||||
# pylint: disable=no-value-for-parameter
|
||||
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_PORT, default=DEFAULT_PORT): cv.port,
|
||||
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_PASSWORD): 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_TIMEOUT),
|
||||
config.get(CONF_SENDER),
|
||||
config.get(CONF_STARTTLS),
|
||||
config.get(CONF_ENCRYPTION),
|
||||
config.get(CONF_USERNAME),
|
||||
config.get(CONF_PASSWORD),
|
||||
config.get(CONF_RECIPIENT),
|
||||
@ -78,28 +81,32 @@ def get_service(hass, config, discovery_info=None):
|
||||
class MailNotificationService(BaseNotificationService):
|
||||
"""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):
|
||||
"""Initialize the SMTP service."""
|
||||
self._server = server
|
||||
self._port = port
|
||||
self._timeout = timeout
|
||||
self._sender = sender
|
||||
self.starttls = starttls
|
||||
self.encryption = encryption
|
||||
self.username = username
|
||||
self.password = password
|
||||
self.recipients = recipients
|
||||
self._sender_name = sender_name
|
||||
self._timeout = timeout
|
||||
self.debug = debug
|
||||
self.tries = 2
|
||||
|
||||
def connect(self):
|
||||
"""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.ehlo_or_helo_if_needed()
|
||||
if self.starttls:
|
||||
if self.encryption == "starttls":
|
||||
mail.starttls()
|
||||
mail.ehlo()
|
||||
if self.username and self.password:
|
||||
|
@ -9,7 +9,7 @@ from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.util import sanitize_filename
|
||||
|
||||
DOMAIN = 'python_script'
|
||||
REQUIREMENTS = ['restrictedpython==4.0a2']
|
||||
REQUIREMENTS = ['restrictedpython==4.0a3']
|
||||
FOLDER = 'python_scripts'
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -33,7 +33,7 @@ from . import purge, migration
|
||||
from .const import DATA_INSTANCE
|
||||
from .util import session_scope
|
||||
|
||||
REQUIREMENTS = ['sqlalchemy==1.1.10']
|
||||
REQUIREMENTS = ['sqlalchemy==1.1.11']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -64,6 +64,8 @@ def _apply_update(engine, new_version):
|
||||
# Create indexes for states
|
||||
_create_index(engine, "states", "ix_states_last_updated")
|
||||
_create_index(engine, "states", "ix_states_entity_id_created")
|
||||
elif new_version == 3:
|
||||
_create_index(engine, "states", "ix_states_created_domain")
|
||||
else:
|
||||
raise ValueError("No schema migration defined for version {}"
|
||||
.format(new_version))
|
||||
|
@ -16,7 +16,7 @@ from homeassistant.remote import JSONEncoder
|
||||
# pylint: disable=invalid-name
|
||||
Base = declarative_base()
|
||||
|
||||
SCHEMA_VERSION = 2
|
||||
SCHEMA_VERSION = 3
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -75,7 +75,9 @@ class States(Base): # type: ignore
|
||||
Index('states__significant_changes',
|
||||
'domain', 'last_updated', 'entity_id'),
|
||||
Index('ix_states_entity_id_created',
|
||||
'entity_id', 'created'),)
|
||||
'entity_id', 'created'),
|
||||
Index('ix_states_created_domain',
|
||||
'created', 'domain'),)
|
||||
|
||||
@staticmethod
|
||||
def from_event(event):
|
||||
|
@ -26,12 +26,13 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_PORT = 5222
|
||||
DEVICES = []
|
||||
CONF_DEVICE_CACHE = 'device_cache'
|
||||
|
||||
SERVICE_SYNC = 'harmony_sync'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
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.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):
|
||||
"""Set up the Harmony platform."""
|
||||
import pyharmony
|
||||
global DEVICES
|
||||
|
||||
name = config.get(CONF_NAME)
|
||||
host = config.get(CONF_HOST)
|
||||
port = config.get(CONF_PORT)
|
||||
_LOGGER.debug("Loading Harmony platform: %s", name)
|
||||
host = None
|
||||
activity = None
|
||||
|
||||
harmony_conf_file = hass.config.path(
|
||||
'{}{}{}'.format('harmony_', slugify(name), '.conf'))
|
||||
if CONF_DEVICE_CACHE not in hass.data:
|
||||
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:
|
||||
_LOGGER.debug("Calling pyharmony.ha_get_token for remote at: %s:%s",
|
||||
host, port)
|
||||
token = urllib.parse.quote_plus(pyharmony.ha_get_token(host, port))
|
||||
address, port)
|
||||
token = urllib.parse.quote_plus(pyharmony.ha_get_token(address, port))
|
||||
_LOGGER.debug("Received token: %s", token)
|
||||
except ValueError as err:
|
||||
_LOGGER.warning("%s for remote: %s", err.args[0], name)
|
||||
return False
|
||||
|
||||
_LOGGER.debug("Received token: %s", token)
|
||||
DEVICES = [HarmonyRemote(
|
||||
config.get(CONF_NAME), config.get(CONF_HOST), config.get(CONF_PORT),
|
||||
config.get(ATTR_ACTIVITY), harmony_conf_file, token)]
|
||||
add_devices(DEVICES, True)
|
||||
harmony_conf_file = hass.config.path(
|
||||
'{}{}{}'.format('harmony_', slugify(name), '.conf'))
|
||||
device = HarmonyRemote(
|
||||
name, address, port,
|
||||
activity, harmony_conf_file, token)
|
||||
|
||||
DEVICES.append(device)
|
||||
|
||||
add_devices([device])
|
||||
register_services(hass)
|
||||
return True
|
||||
|
||||
|
@ -10,9 +10,12 @@ import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
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.const import (ATTR_ENTITY_ID, TEMP_CELSIUS)
|
||||
|
||||
REQUIREMENTS = ['pyRFXtrx==0.18.0']
|
||||
|
||||
@ -27,7 +30,9 @@ ATTR_STATE = 'state'
|
||||
ATTR_NAME = 'name'
|
||||
ATTR_FIREEVENT = 'fire_event'
|
||||
ATTR_DATA_TYPE = 'data_type'
|
||||
ATTR_DATA_BITS = 'data_bits'
|
||||
ATTR_DUMMY = 'dummy'
|
||||
ATTR_OFF_DELAY = 'off_delay'
|
||||
CONF_SIGNAL_REPETITIONS = 'signal_repetitions'
|
||||
CONF_DEVICES = 'devices'
|
||||
EVENT_BUTTON_PRESSED = 'button_pressed'
|
||||
@ -43,7 +48,8 @@ DATA_TYPES = OrderedDict([
|
||||
('Total usage', 'W'),
|
||||
('Sound', ''),
|
||||
('Sensor Status', ''),
|
||||
('Counter value', '')])
|
||||
('Counter value', ''),
|
||||
('UV', 'uv')])
|
||||
|
||||
RECEIVED_EVT_SUBSCRIBERS = []
|
||||
RFX_DEVICES = {}
|
||||
@ -77,6 +83,8 @@ def _valid_device(value, device_type):
|
||||
|
||||
if device_type == 'sensor':
|
||||
config[key] = DEVICE_SCHEMA_SENSOR(device)
|
||||
elif device_type == 'binary_sensor':
|
||||
config[key] = DEVICE_SCHEMA_BINARYSENSOR(device)
|
||||
elif device_type == 'light_switch':
|
||||
config[key] = DEVICE_SCHEMA(device)
|
||||
else:
|
||||
@ -92,6 +100,11 @@ def valid_sensor(value):
|
||||
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):
|
||||
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())]),
|
||||
})
|
||||
|
||||
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({
|
||||
vol.Required("platform"): DOMAIN,
|
||||
vol.Optional(CONF_DEVICES, default={}): vol.All(dict, _valid_light_switch),
|
||||
@ -191,6 +215,78 @@ def get_rfx_object(packetid):
|
||||
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):
|
||||
"""Read rfxtrx configuration."""
|
||||
signal_repetitions = config[CONF_SIGNAL_REPETITIONS]
|
||||
@ -318,6 +414,11 @@ class RfxtrxDevice(Entity):
|
||||
"""Return is the device must fire event."""
|
||||
return self._should_fire_event
|
||||
|
||||
@property
|
||||
def is_pt2262(self):
|
||||
"""Return true if the device is PT2262-based."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if device is on."""
|
||||
|
@ -102,7 +102,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
pin=pinnum, unit_of_measurement=pin.get(
|
||||
CONF_UNIT_OF_MEASUREMENT), renderer=renderer))
|
||||
|
||||
add_devices(dev)
|
||||
add_devices(dev, True)
|
||||
|
||||
|
||||
class ArestSensor(Entity):
|
||||
@ -119,7 +119,6 @@ class ArestSensor(Entity):
|
||||
self._state = STATE_UNKNOWN
|
||||
self._unit_of_measurement = unit_of_measurement
|
||||
self._renderer = renderer
|
||||
self.update()
|
||||
|
||||
if self._pin is not None:
|
||||
request = requests.get(
|
||||
|
145
homeassistant/components/sensor/bh1750.py
Normal file
145
homeassistant/components/sensor/bh1750.py
Normal 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
Loading…
x
Reference in New Issue
Block a user