Merge branch 'dev' into nuheat

This commit is contained in:
Derek Brooks 2017-12-24 09:40:21 -07:00
commit bdf64ccbbb
122 changed files with 5592 additions and 1210 deletions

View File

@ -88,7 +88,7 @@ omit =
homeassistant/components/hive.py homeassistant/components/hive.py
homeassistant/components/*/hive.py homeassistant/components/*/hive.py
homeassistant/components/homematic.py homeassistant/components/homematic/__init__.py
homeassistant/components/*/homematic.py homeassistant/components/*/homematic.py
homeassistant/components/insteon_local.py homeassistant/components/insteon_local.py
@ -264,6 +264,7 @@ omit =
homeassistant/components/*/zoneminder.py homeassistant/components/*/zoneminder.py
homeassistant/components/alarm_control_panel/alarmdotcom.py homeassistant/components/alarm_control_panel/alarmdotcom.py
homeassistant/components/alarm_control_panel/canary.py
homeassistant/components/alarm_control_panel/concord232.py homeassistant/components/alarm_control_panel/concord232.py
homeassistant/components/alarm_control_panel/egardia.py homeassistant/components/alarm_control_panel/egardia.py
homeassistant/components/alarm_control_panel/ialarm.py homeassistant/components/alarm_control_panel/ialarm.py
@ -283,8 +284,10 @@ omit =
homeassistant/components/binary_sensor/rest.py homeassistant/components/binary_sensor/rest.py
homeassistant/components/binary_sensor/tapsaff.py homeassistant/components/binary_sensor/tapsaff.py
homeassistant/components/browser.py homeassistant/components/browser.py
homeassistant/components/calendar/caldav.py
homeassistant/components/calendar/todoist.py homeassistant/components/calendar/todoist.py
homeassistant/components/camera/bloomsky.py homeassistant/components/camera/bloomsky.py
homeassistant/components/camera/canary.py
homeassistant/components/camera/ffmpeg.py homeassistant/components/camera/ffmpeg.py
homeassistant/components/camera/foscam.py homeassistant/components/camera/foscam.py
homeassistant/components/camera/mjpeg.py homeassistant/components/camera/mjpeg.py
@ -362,6 +365,7 @@ omit =
homeassistant/components/light/decora.py homeassistant/components/light/decora.py
homeassistant/components/light/decora_wifi.py homeassistant/components/light/decora_wifi.py
homeassistant/components/light/flux_led.py homeassistant/components/light/flux_led.py
homeassistant/components/light/greenwave.py
homeassistant/components/light/hue.py homeassistant/components/light/hue.py
homeassistant/components/light/hyperion.py homeassistant/components/light/hyperion.py
homeassistant/components/light/lifx.py homeassistant/components/light/lifx.py
@ -425,6 +429,7 @@ omit =
homeassistant/components/media_player/sonos.py homeassistant/components/media_player/sonos.py
homeassistant/components/media_player/spotify.py homeassistant/components/media_player/spotify.py
homeassistant/components/media_player/squeezebox.py homeassistant/components/media_player/squeezebox.py
homeassistant/components/media_player/ue_smart_radio.py
homeassistant/components/media_player/vizio.py homeassistant/components/media_player/vizio.py
homeassistant/components/media_player/vlc.py homeassistant/components/media_player/vlc.py
homeassistant/components/media_player/volumio.py homeassistant/components/media_player/volumio.py
@ -500,6 +505,7 @@ omit =
homeassistant/components/sensor/deluge.py homeassistant/components/sensor/deluge.py
homeassistant/components/sensor/deutsche_bahn.py homeassistant/components/sensor/deutsche_bahn.py
homeassistant/components/sensor/dht.py homeassistant/components/sensor/dht.py
homeassistant/components/sensor/discogs.py
homeassistant/components/sensor/dnsip.py homeassistant/components/sensor/dnsip.py
homeassistant/components/sensor/dovado.py homeassistant/components/sensor/dovado.py
homeassistant/components/sensor/dte_energy_bridge.py homeassistant/components/sensor/dte_energy_bridge.py
@ -528,7 +534,6 @@ omit =
homeassistant/components/sensor/haveibeenpwned.py homeassistant/components/sensor/haveibeenpwned.py
homeassistant/components/sensor/hp_ilo.py homeassistant/components/sensor/hp_ilo.py
homeassistant/components/sensor/htu21d.py homeassistant/components/sensor/htu21d.py
homeassistant/components/sensor/hydroquebec.py
homeassistant/components/sensor/imap.py homeassistant/components/sensor/imap.py
homeassistant/components/sensor/imap_email_content.py homeassistant/components/sensor/imap_email_content.py
homeassistant/components/sensor/influxdb.py homeassistant/components/sensor/influxdb.py

View File

@ -53,10 +53,11 @@ homeassistant/components/light/yeelight.py @rytilahti
homeassistant/components/media_player/kodi.py @armills homeassistant/components/media_player/kodi.py @armills
homeassistant/components/media_player/monoprice.py @etsinko homeassistant/components/media_player/monoprice.py @etsinko
homeassistant/components/media_player/yamaha_musiccast.py @jalmeroth homeassistant/components/media_player/yamaha_musiccast.py @jalmeroth
homeassistant/components/plant.py @ChristianKuehnel
homeassistant/components/sensor/airvisual.py @bachya homeassistant/components/sensor/airvisual.py @bachya
homeassistant/components/sensor/gearbest.py @HerrHofrat homeassistant/components/sensor/gearbest.py @HerrHofrat
homeassistant/components/sensor/irish_rail_transport.py @ttroy50 homeassistant/components/sensor/irish_rail_transport.py @ttroy50
homeassistant/components/sensor/miflora.py @danielhiversen homeassistant/components/sensor/miflora.py @danielhiversen @ChristianKuehnel
homeassistant/components/sensor/sytadin.py @gautric homeassistant/components/sensor/sytadin.py @gautric
homeassistant/components/sensor/tibber.py @danielhiversen homeassistant/components/sensor/tibber.py @danielhiversen
homeassistant/components/sensor/waqi.py @andrey-git homeassistant/components/sensor/waqi.py @andrey-git

View File

@ -230,7 +230,7 @@ def closefds_osx(min_fd: int, max_fd: int) -> None:
def cmdline() -> List[str]: def cmdline() -> List[str]:
"""Collect path and arguments to re-execute the current hass instance.""" """Collect path and arguments to re-execute the current hass instance."""
if sys.argv[0].endswith(os.path.sep + '__main__.py'): if os.path.basename(sys.argv[0]) == '__main__.py':
modulepath = os.path.dirname(sys.argv[0]) modulepath = os.path.dirname(sys.argv[0])
os.environ['PYTHONPATH'] = os.path.dirname(modulepath) os.environ['PYTHONPATH'] = os.path.dirname(modulepath)
return [sys.executable] + [arg for arg in sys.argv if return [sys.executable] + [arg for arg in sys.argv if

View File

@ -7,30 +7,21 @@ https://home-assistant.io/components/alarm_control_panel.alarmdecoder/
import asyncio import asyncio
import logging import logging
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
import homeassistant.components.alarm_control_panel as alarm import homeassistant.components.alarm_control_panel as alarm
from homeassistant.components.alarmdecoder import (
from homeassistant.components.alarmdecoder import (DATA_AD, DATA_AD, SIGNAL_PANEL_MESSAGE)
SIGNAL_PANEL_MESSAGE)
from homeassistant.const import ( from homeassistant.const import (
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED,
STATE_UNKNOWN, STATE_ALARM_TRIGGERED) STATE_ALARM_TRIGGERED)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['alarmdecoder'] DEPENDENCIES = ['alarmdecoder']
@asyncio.coroutine def setup_platform(hass, config, add_devices, discovery_info=None):
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up for AlarmDecoder alarm panels.""" """Set up for AlarmDecoder alarm panels."""
_LOGGER.debug("AlarmDecoderAlarmPanel: setup") add_devices([AlarmDecoderAlarmPanel()])
device = AlarmDecoderAlarmPanel("Alarm Panel", hass)
async_add_devices([device])
return True return True
@ -38,38 +29,35 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
class AlarmDecoderAlarmPanel(alarm.AlarmControlPanel): class AlarmDecoderAlarmPanel(alarm.AlarmControlPanel):
"""Representation of an AlarmDecoder-based alarm panel.""" """Representation of an AlarmDecoder-based alarm panel."""
def __init__(self, name, hass): def __init__(self):
"""Initialize the alarm panel.""" """Initialize the alarm panel."""
self._display = "" self._display = ""
self._name = name self._name = "Alarm Panel"
self._state = STATE_UNKNOWN self._state = None
_LOGGER.debug("Setting up panel")
@asyncio.coroutine @asyncio.coroutine
def async_added_to_hass(self): def async_added_to_hass(self):
"""Register callbacks.""" """Register callbacks."""
async_dispatcher_connect( self.hass.helpers.dispatcher.async_dispatcher_connect(
self.hass, SIGNAL_PANEL_MESSAGE, self._message_callback) SIGNAL_PANEL_MESSAGE, self._message_callback)
@callback
def _message_callback(self, message): def _message_callback(self, message):
if message.alarm_sounding or message.fire_alarm: if message.alarm_sounding or message.fire_alarm:
if self._state != STATE_ALARM_TRIGGERED: if self._state != STATE_ALARM_TRIGGERED:
self._state = STATE_ALARM_TRIGGERED self._state = STATE_ALARM_TRIGGERED
self.async_schedule_update_ha_state() self.schedule_update_ha_state()
elif message.armed_away: elif message.armed_away:
if self._state != STATE_ALARM_ARMED_AWAY: if self._state != STATE_ALARM_ARMED_AWAY:
self._state = STATE_ALARM_ARMED_AWAY self._state = STATE_ALARM_ARMED_AWAY
self.async_schedule_update_ha_state() self.schedule_update_ha_state()
elif message.armed_home: elif message.armed_home:
if self._state != STATE_ALARM_ARMED_HOME: if self._state != STATE_ALARM_ARMED_HOME:
self._state = STATE_ALARM_ARMED_HOME self._state = STATE_ALARM_ARMED_HOME
self.async_schedule_update_ha_state() self.schedule_update_ha_state()
else: else:
if self._state != STATE_ALARM_DISARMED: if self._state != STATE_ALARM_DISARMED:
self._state = STATE_ALARM_DISARMED self._state = STATE_ALARM_DISARMED
self.async_schedule_update_ha_state() self.schedule_update_ha_state()
@property @property
def name(self): def name(self):
@ -91,26 +79,20 @@ class AlarmDecoderAlarmPanel(alarm.AlarmControlPanel):
"""Return the state of the device.""" """Return the state of the device."""
return self._state return self._state
@asyncio.coroutine def alarm_disarm(self, code=None):
def async_alarm_disarm(self, code=None):
"""Send disarm command.""" """Send disarm command."""
_LOGGER.debug("alarm_disarm: %s", code)
if code: if code:
_LOGGER.debug("alarm_disarm: sending %s1", str(code)) _LOGGER.debug("alarm_disarm: sending %s1", str(code))
self.hass.data[DATA_AD].send("{!s}1".format(code)) self.hass.data[DATA_AD].send("{!s}1".format(code))
@asyncio.coroutine def alarm_arm_away(self, code=None):
def async_alarm_arm_away(self, code=None):
"""Send arm away command.""" """Send arm away command."""
_LOGGER.debug("alarm_arm_away: %s", code)
if code: if code:
_LOGGER.debug("alarm_arm_away: sending %s2", str(code)) _LOGGER.debug("alarm_arm_away: sending %s2", str(code))
self.hass.data[DATA_AD].send("{!s}2".format(code)) self.hass.data[DATA_AD].send("{!s}2".format(code))
@asyncio.coroutine def alarm_arm_home(self, code=None):
def async_alarm_arm_home(self, code=None):
"""Send arm home command.""" """Send arm home command."""
_LOGGER.debug("alarm_arm_home: %s", code)
if code: if code:
_LOGGER.debug("alarm_arm_home: sending %s3", str(code)) _LOGGER.debug("alarm_arm_home: sending %s3", str(code))
self.hass.data[DATA_AD].send("{!s}3".format(code)) self.hass.data[DATA_AD].send("{!s}3".format(code))

View File

@ -0,0 +1,92 @@
"""
Support for Canary alarm.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/alarm_control_panel.canary/
"""
import logging
from homeassistant.components.alarm_control_panel import AlarmControlPanel
from homeassistant.components.canary import DATA_CANARY
from homeassistant.const import STATE_ALARM_DISARMED, STATE_ALARM_ARMED_AWAY, \
STATE_ALARM_ARMED_NIGHT, STATE_ALARM_ARMED_HOME
DEPENDENCIES = ['canary']
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Canary alarms."""
data = hass.data[DATA_CANARY]
devices = []
for location in data.locations:
devices.append(CanaryAlarm(data, location.location_id))
add_devices(devices, True)
class CanaryAlarm(AlarmControlPanel):
"""Representation of a Canary alarm control panel."""
def __init__(self, data, location_id):
"""Initialize a Canary security camera."""
self._data = data
self._location_id = location_id
@property
def name(self):
"""Return the name of the alarm."""
location = self._data.get_location(self._location_id)
return location.name
@property
def state(self):
"""Return the state of the device."""
from canary.api import LOCATION_MODE_AWAY, LOCATION_MODE_HOME, \
LOCATION_MODE_NIGHT
location = self._data.get_location(self._location_id)
if location.is_private:
return STATE_ALARM_DISARMED
mode = location.mode
if mode.name == LOCATION_MODE_AWAY:
return STATE_ALARM_ARMED_AWAY
elif mode.name == LOCATION_MODE_HOME:
return STATE_ALARM_ARMED_HOME
elif mode.name == LOCATION_MODE_NIGHT:
return STATE_ALARM_ARMED_NIGHT
else:
return None
@property
def device_state_attributes(self):
"""Return the state attributes."""
location = self._data.get_location(self._location_id)
return {
'private': location.is_private
}
def alarm_disarm(self, code=None):
"""Send disarm command."""
location = self._data.get_location(self._location_id)
self._data.set_location_mode(self._location_id, location.mode.name,
True)
def alarm_arm_home(self, code=None):
"""Send arm home command."""
from canary.api import LOCATION_MODE_HOME
self._data.set_location_mode(self._location_id, LOCATION_MODE_HOME)
def alarm_arm_away(self, code=None):
"""Send arm away command."""
from canary.api import LOCATION_MODE_AWAY
self._data.set_location_mode(self._location_id, LOCATION_MODE_AWAY)
def alarm_arm_night(self, code=None):
"""Send arm night command."""
from canary.api import LOCATION_MODE_NIGHT
self._data.set_location_mode(self._location_id, LOCATION_MODE_NIGHT)

View File

@ -116,12 +116,20 @@ class EgardiaAlarm(alarm.AlarmControlPanel):
"""Return the state of the device.""" """Return the state of the device."""
return self._status return self._status
@property
def should_poll(self):
"""Poll if no report server is enabled."""
if not self._rs_enabled:
return True
return False
def handle_system_status_event(self, event): def handle_system_status_event(self, event):
"""Handle egardia_system_status_event.""" """Handle egardia_system_status_event."""
if event.data.get('status') is not None: if event.data.get('status') is not None:
statuscode = event.data.get('status') statuscode = event.data.get('status')
status = self.lookupstatusfromcode(statuscode) status = self.lookupstatusfromcode(statuscode)
self.parsestatus(status) self.parsestatus(status)
self.schedule_update_ha_state()
def listen_to_system_status(self): def listen_to_system_status(self):
"""Subscribe to egardia_system_status event.""" """Subscribe to egardia_system_status event."""
@ -161,9 +169,8 @@ class EgardiaAlarm(alarm.AlarmControlPanel):
def update(self): def update(self):
"""Update the alarm status.""" """Update the alarm status."""
if not self._rs_enabled: status = self._egardiasystem.getstate()
status = self._egardiasystem.getstate() self.parsestatus(status)
self.parsestatus(status)
def alarm_disarm(self, code=None): def alarm_disarm(self, code=None):
"""Send disarm command.""" """Send disarm command."""

View File

@ -14,7 +14,9 @@ from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA
from homeassistant.const import ( from homeassistant.const import (
CONF_PASSWORD, CONF_USERNAME, STATE_ALARM_ARMED_AWAY, CONF_PASSWORD, CONF_USERNAME, STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED,
STATE_ALARM_ARMING, STATE_ALARM_DISARMING, STATE_UNKNOWN, CONF_NAME) STATE_ALARM_ARMING, STATE_ALARM_DISARMING, STATE_UNKNOWN, CONF_NAME,
STATE_ALARM_ARMED_CUSTOM_BYPASS)
REQUIREMENTS = ['total_connect_client==0.16'] REQUIREMENTS = ['total_connect_client==0.16']
@ -76,6 +78,8 @@ class TotalConnect(alarm.AlarmControlPanel):
state = STATE_ALARM_ARMED_AWAY state = STATE_ALARM_ARMED_AWAY
elif status == self._client.ARMED_STAY_NIGHT: elif status == self._client.ARMED_STAY_NIGHT:
state = STATE_ALARM_ARMED_NIGHT state = STATE_ALARM_ARMED_NIGHT
elif status == self._client.ARMED_CUSTOM_BYPASS:
state = STATE_ALARM_ARMED_CUSTOM_BYPASS
elif status == self._client.ARMING: elif status == self._client.ARMING:
state = STATE_ALARM_ARMING state = STATE_ALARM_ARMING
elif status == self._client.DISARMING: elif status == self._client.DISARMING:

View File

@ -4,16 +4,13 @@ Support for AlarmDecoder devices.
For more details about this component, please refer to the documentation at For more details about this component, please refer to the documentation at
https://home-assistant.io/components/alarmdecoder/ https://home-assistant.io/components/alarmdecoder/
""" """
import asyncio
import logging import logging
import voluptuous as vol import voluptuous as vol
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.core import callback
from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.discovery import load_platform
from homeassistant.helpers.dispatcher import async_dispatcher_send
REQUIREMENTS = ['alarmdecoder==0.12.3'] REQUIREMENTS = ['alarmdecoder==0.12.3']
@ -71,9 +68,9 @@ ZONE_SCHEMA = vol.Schema({
CONFIG_SCHEMA = vol.Schema({ CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({ DOMAIN: vol.Schema({
vol.Required(CONF_DEVICE): vol.Any(DEVICE_SOCKET_SCHEMA, vol.Required(CONF_DEVICE): vol.Any(
DEVICE_SERIAL_SCHEMA, DEVICE_SOCKET_SCHEMA, DEVICE_SERIAL_SCHEMA,
DEVICE_USB_SCHEMA), DEVICE_USB_SCHEMA),
vol.Optional(CONF_PANEL_DISPLAY, vol.Optional(CONF_PANEL_DISPLAY,
default=DEFAULT_PANEL_DISPLAY): cv.boolean, default=DEFAULT_PANEL_DISPLAY): cv.boolean,
vol.Optional(CONF_ZONES): {vol.Coerce(int): ZONE_SCHEMA}, vol.Optional(CONF_ZONES): {vol.Coerce(int): ZONE_SCHEMA},
@ -81,8 +78,7 @@ CONFIG_SCHEMA = vol.Schema({
}, extra=vol.ALLOW_EXTRA) }, extra=vol.ALLOW_EXTRA)
@asyncio.coroutine def setup(hass, config):
def async_setup(hass, config):
"""Set up for the AlarmDecoder devices.""" """Set up for the AlarmDecoder devices."""
from alarmdecoder import AlarmDecoder from alarmdecoder import AlarmDecoder
from alarmdecoder.devices import (SocketDevice, SerialDevice, USBDevice) from alarmdecoder.devices import (SocketDevice, SerialDevice, USBDevice)
@ -99,32 +95,25 @@ def async_setup(hass, config):
path = DEFAULT_DEVICE_PATH path = DEFAULT_DEVICE_PATH
baud = DEFAULT_DEVICE_BAUD baud = DEFAULT_DEVICE_BAUD
sync_connect = asyncio.Future(loop=hass.loop)
def handle_open(device):
"""Handle the successful connection."""
_LOGGER.info("Established a connection with the alarmdecoder")
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_alarmdecoder)
sync_connect.set_result(True)
@callback
def stop_alarmdecoder(event): def stop_alarmdecoder(event):
"""Handle the shutdown of AlarmDecoder.""" """Handle the shutdown of AlarmDecoder."""
_LOGGER.debug("Shutting down alarmdecoder") _LOGGER.debug("Shutting down alarmdecoder")
controller.close() controller.close()
@callback
def handle_message(sender, message): def handle_message(sender, message):
"""Handle message from AlarmDecoder.""" """Handle message from AlarmDecoder."""
async_dispatcher_send(hass, SIGNAL_PANEL_MESSAGE, message) hass.helpers.dispatcher.dispatcher_send(
SIGNAL_PANEL_MESSAGE, message)
def zone_fault_callback(sender, zone): def zone_fault_callback(sender, zone):
"""Handle zone fault from AlarmDecoder.""" """Handle zone fault from AlarmDecoder."""
async_dispatcher_send(hass, SIGNAL_ZONE_FAULT, zone) hass.helpers.dispatcher.dispatcher_send(
SIGNAL_ZONE_FAULT, zone)
def zone_restore_callback(sender, zone): def zone_restore_callback(sender, zone):
"""Handle zone restore from AlarmDecoder.""" """Handle zone restore from AlarmDecoder."""
async_dispatcher_send(hass, SIGNAL_ZONE_RESTORE, zone) hass.helpers.dispatcher.dispatcher_send(
SIGNAL_ZONE_RESTORE, zone)
controller = False controller = False
if device_type == 'socket': if device_type == 'socket':
@ -139,7 +128,6 @@ def async_setup(hass, config):
AlarmDecoder(USBDevice.find()) AlarmDecoder(USBDevice.find())
return False return False
controller.on_open += handle_open
controller.on_message += handle_message controller.on_message += handle_message
controller.on_zone_fault += zone_fault_callback controller.on_zone_fault += zone_fault_callback
controller.on_zone_restore += zone_restore_callback controller.on_zone_restore += zone_restore_callback
@ -148,21 +136,16 @@ def async_setup(hass, config):
controller.open(baud) controller.open(baud)
result = yield from sync_connect _LOGGER.debug("Established a connection with the alarmdecoder")
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_alarmdecoder)
if not result: load_platform(hass, 'alarm_control_panel', DOMAIN, conf, config)
return False
hass.async_add_job(
async_load_platform(hass, 'alarm_control_panel', DOMAIN, conf,
config))
if zones: if zones:
hass.async_add_job(async_load_platform( load_platform(
hass, 'binary_sensor', DOMAIN, {CONF_ZONES: zones}, config)) hass, 'binary_sensor', DOMAIN, {CONF_ZONES: zones}, config)
if display: if display:
hass.async_add_job(async_load_platform( load_platform(hass, 'sensor', DOMAIN, conf, config)
hass, 'sensor', DOMAIN, conf, config))
return True return True

View File

@ -18,7 +18,7 @@ from homeassistant.helpers import discovery
from homeassistant.components.discovery import SERVICE_APPLE_TV from homeassistant.components.discovery import SERVICE_APPLE_TV
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['pyatv==0.3.8'] REQUIREMENTS = ['pyatv==0.3.9']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -34,6 +34,7 @@ DEVICE_CLASSES = [
'plug', # On means plugged in, Off means unplugged 'plug', # On means plugged in, Off means unplugged
'power', # Power, over-current, etc 'power', # Power, over-current, etc
'presence', # On means home, Off means away 'presence', # On means home, Off means away
'problem', # On means there is a problem, Off means the status is OK
'safety', # Generic on=unsafe, off=safe 'safety', # Generic on=unsafe, off=safe
'smoke', # Smoke detector 'smoke', # Smoke detector
'sound', # On means sound detected, Off means no sound 'sound', # On means sound detected, Off means no sound

View File

@ -7,39 +7,29 @@ https://home-assistant.io/components/binary_sensor.alarmdecoder/
import asyncio import asyncio
import logging import logging
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.alarmdecoder import (
from homeassistant.components.alarmdecoder import (ZONE_SCHEMA, ZONE_SCHEMA, CONF_ZONES, CONF_ZONE_NAME, CONF_ZONE_TYPE,
CONF_ZONES, SIGNAL_ZONE_FAULT, SIGNAL_ZONE_RESTORE)
CONF_ZONE_NAME,
CONF_ZONE_TYPE,
SIGNAL_ZONE_FAULT,
SIGNAL_ZONE_RESTORE)
DEPENDENCIES = ['alarmdecoder'] DEPENDENCIES = ['alarmdecoder']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@asyncio.coroutine def setup_platform(hass, config, add_devices, discovery_info=None):
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up the AlarmDecoder binary sensor devices.""" """Set up the AlarmDecoder binary sensor devices."""
configured_zones = discovery_info[CONF_ZONES] configured_zones = discovery_info[CONF_ZONES]
devices = [] devices = []
for zone_num in configured_zones: for zone_num in configured_zones:
device_config_data = ZONE_SCHEMA(configured_zones[zone_num]) device_config_data = ZONE_SCHEMA(configured_zones[zone_num])
zone_type = device_config_data[CONF_ZONE_TYPE] zone_type = device_config_data[CONF_ZONE_TYPE]
zone_name = device_config_data[CONF_ZONE_NAME] zone_name = device_config_data[CONF_ZONE_NAME]
device = AlarmDecoderBinarySensor( device = AlarmDecoderBinarySensor(zone_num, zone_name, zone_type)
hass, zone_num, zone_name, zone_type)
devices.append(device) devices.append(device)
async_add_devices(devices) add_devices(devices)
return True return True
@ -47,7 +37,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
class AlarmDecoderBinarySensor(BinarySensorDevice): class AlarmDecoderBinarySensor(BinarySensorDevice):
"""Representation of an AlarmDecoder binary sensor.""" """Representation of an AlarmDecoder binary sensor."""
def __init__(self, hass, zone_number, zone_name, zone_type): def __init__(self, zone_number, zone_name, zone_type):
"""Initialize the binary_sensor.""" """Initialize the binary_sensor."""
self._zone_number = zone_number self._zone_number = zone_number
self._zone_type = zone_type self._zone_type = zone_type
@ -55,16 +45,14 @@ class AlarmDecoderBinarySensor(BinarySensorDevice):
self._name = zone_name self._name = zone_name
self._type = zone_type self._type = zone_type
_LOGGER.debug("Setup up zone: %s", self._name)
@asyncio.coroutine @asyncio.coroutine
def async_added_to_hass(self): def async_added_to_hass(self):
"""Register callbacks.""" """Register callbacks."""
async_dispatcher_connect( self.hass.helpers.dispatcher.async_dispatcher_connect(
self.hass, SIGNAL_ZONE_FAULT, self._fault_callback) SIGNAL_ZONE_FAULT, self._fault_callback)
async_dispatcher_connect( self.hass.helpers.dispatcher.async_dispatcher_connect(
self.hass, SIGNAL_ZONE_RESTORE, self._restore_callback) SIGNAL_ZONE_RESTORE, self._restore_callback)
@property @property
def name(self): def name(self):
@ -97,16 +85,14 @@ class AlarmDecoderBinarySensor(BinarySensorDevice):
"""Return the class of this sensor, from DEVICE_CLASSES.""" """Return the class of this sensor, from DEVICE_CLASSES."""
return self._zone_type return self._zone_type
@callback
def _fault_callback(self, zone): def _fault_callback(self, zone):
"""Update the zone's state, if needed.""" """Update the zone's state, if needed."""
if zone is None or int(zone) == self._zone_number: if zone is None or int(zone) == self._zone_number:
self._state = 1 self._state = 1
self.async_schedule_update_ha_state() self.schedule_update_ha_state()
@callback
def _restore_callback(self, zone): def _restore_callback(self, zone):
"""Update the zone's state, if needed.""" """Update the zone's state, if needed."""
if zone is None or int(zone) == self._zone_number: if zone is None or int(zone) == self._zone_number:
self._state = 0 self._state = 0
self.async_schedule_update_ha_state() self.schedule_update_ha_state()

View File

@ -118,7 +118,7 @@ class Concord232ZoneSensor(BinarySensorDevice):
def is_on(self): def is_on(self):
"""Return true if the binary sensor is on.""" """Return true if the binary sensor is on."""
# True means "faulted" or "open" or "abnormal state" # True means "faulted" or "open" or "abnormal state"
return bool(self._zone['state'] == 'Normal') return bool(self._zone['state'] != 'Normal')
def update(self): def update(self):
"""Get updated stats from API.""" """Get updated stats from API."""

View File

@ -4,24 +4,31 @@ Support for ISY994 binary sensors.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.isy994/ https://home-assistant.io/components/binary_sensor.isy994/
""" """
import asyncio
import logging import logging
from datetime import timedelta
from typing import Callable # noqa from typing import Callable # noqa
from homeassistant.core import callback
from homeassistant.components.binary_sensor import BinarySensorDevice, DOMAIN from homeassistant.components.binary_sensor import BinarySensorDevice, DOMAIN
import homeassistant.components.isy994 as isy import homeassistant.components.isy994 as isy
from homeassistant.const import STATE_ON, STATE_OFF from homeassistant.const import STATE_ON, STATE_OFF
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
from homeassistant.helpers.event import async_track_point_in_utc_time
from homeassistant.util import dt as dt_util
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
VALUE_TO_STATE = {
False: STATE_OFF,
True: STATE_ON,
}
UOM = ['2', '78'] UOM = ['2', '78']
STATES = [STATE_OFF, STATE_ON, 'true', 'false'] STATES = [STATE_OFF, STATE_ON, 'true', 'false']
ISY_DEVICE_TYPES = {
'moisture': ['16.8', '16.13', '16.14'],
'opening': ['16.9', '16.6', '16.7', '16.2', '16.17', '16.20', '16.21'],
'motion': ['16.1', '16.4', '16.5', '16.3']
}
# pylint: disable=unused-argument # pylint: disable=unused-argument
def setup_platform(hass, config: ConfigType, def setup_platform(hass, config: ConfigType,
@ -32,10 +39,46 @@ def setup_platform(hass, config: ConfigType,
return False return False
devices = [] devices = []
devices_by_nid = {}
child_nodes = []
for node in isy.filter_nodes(isy.SENSOR_NODES, units=UOM, for node in isy.filter_nodes(isy.SENSOR_NODES, units=UOM,
states=STATES): states=STATES):
devices.append(ISYBinarySensorDevice(node)) if node.parent_node is None:
device = ISYBinarySensorDevice(node)
devices.append(device)
devices_by_nid[node.nid] = device
else:
# We'll process the child nodes last, to ensure all parent nodes
# have been processed
child_nodes.append(node)
for node in child_nodes:
try:
parent_device = devices_by_nid[node.parent_node.nid]
except KeyError:
_LOGGER.error("Node %s has a parent node %s, but no device "
"was created for the parent. Skipping.",
node.nid, node.parent_nid)
else:
device_type = _detect_device_type(node)
if device_type in ['moisture', 'opening']:
subnode_id = int(node.nid[-1])
# Leak and door/window sensors work the same way with negative
# nodes and heartbeat nodes
if subnode_id == 4:
# Subnode 4 is the heartbeat node, which we will represent
# as a separate binary_sensor
device = ISYBinarySensorHeartbeat(node, parent_device)
parent_device.add_heartbeat_device(device)
devices.append(device)
elif subnode_id == 2:
parent_device.add_negative_node(node)
else:
# We don't yet have any special logic for other sensor types,
# so add the nodes as individual devices
device = ISYBinarySensorDevice(node)
devices.append(device)
for program in isy.PROGRAMS.get(DOMAIN, []): for program in isy.PROGRAMS.get(DOMAIN, []):
try: try:
@ -48,23 +91,282 @@ def setup_platform(hass, config: ConfigType,
add_devices(devices) add_devices(devices)
def _detect_device_type(node) -> str:
try:
device_type = node.type
except AttributeError:
# The type attribute didn't exist in the ISY's API response
return None
split_type = device_type.split('.')
for device_class, ids in ISY_DEVICE_TYPES.items():
if '{}.{}'.format(split_type[0], split_type[1]) in ids:
return device_class
return None
def _is_val_unknown(val):
"""Determine if a number value represents UNKNOWN from PyISY."""
return val == -1*float('inf')
class ISYBinarySensorDevice(isy.ISYDevice, BinarySensorDevice): class ISYBinarySensorDevice(isy.ISYDevice, BinarySensorDevice):
"""Representation of an ISY994 binary sensor device.""" """Representation of an ISY994 binary sensor device.
Often times, a single device is represented by multiple nodes in the ISY,
allowing for different nuances in how those devices report their on and
off events. This class turns those multiple nodes in to a single Hass
entity and handles both ways that ISY binary sensors can work.
"""
def __init__(self, node) -> None: def __init__(self, node) -> None:
"""Initialize the ISY994 binary sensor device.""" """Initialize the ISY994 binary sensor device."""
isy.ISYDevice.__init__(self, node) super().__init__(node)
self._negative_node = None
self._heartbeat_device = None
self._device_class_from_type = _detect_device_type(self._node)
# pylint: disable=protected-access
if _is_val_unknown(self._node.status._val):
self._computed_state = None
else:
self._computed_state = bool(self._node.status._val)
@asyncio.coroutine
def async_added_to_hass(self) -> None:
"""Subscribe to the node and subnode event emitters."""
yield from super().async_added_to_hass()
self._node.controlEvents.subscribe(self._positive_node_control_handler)
if self._negative_node is not None:
self._negative_node.controlEvents.subscribe(
self._negative_node_control_handler)
def add_heartbeat_device(self, device) -> None:
"""Register a heartbeat device for this sensor.
The heartbeat node beats on its own, but we can gain a little
reliability by considering any node activity for this sensor
to be a heartbeat as well.
"""
self._heartbeat_device = device
def _heartbeat(self) -> None:
"""Send a heartbeat to our heartbeat device, if we have one."""
if self._heartbeat_device is not None:
self._heartbeat_device.heartbeat()
def add_negative_node(self, child) -> None:
"""Add a negative node to this binary sensor device.
The negative node is a node that can receive the 'off' events
for the sensor, depending on device configuration and type.
"""
self._negative_node = child
# pylint: disable=protected-access
if not _is_val_unknown(self._negative_node.status._val):
# If the negative node has a value, it means the negative node is
# in use for this device. Therefore, we cannot determine the state
# of the sensor until we receive our first ON event.
self._computed_state = None
def _negative_node_control_handler(self, event: object) -> None:
"""Handle an "On" control event from the "negative" node."""
if event == 'DON':
_LOGGER.debug("Sensor %s turning Off via the Negative node "
"sending a DON command", self.name)
self._computed_state = False
self.schedule_update_ha_state()
self._heartbeat()
def _positive_node_control_handler(self, event: object) -> None:
"""Handle On and Off control event coming from the primary node.
Depending on device configuration, sometimes only On events
will come to this node, with the negative node representing Off
events
"""
if event == 'DON':
_LOGGER.debug("Sensor %s turning On via the Primary node "
"sending a DON command", self.name)
self._computed_state = True
self.schedule_update_ha_state()
self._heartbeat()
if event == 'DOF':
_LOGGER.debug("Sensor %s turning Off via the Primary node "
"sending a DOF command", self.name)
self._computed_state = False
self.schedule_update_ha_state()
self._heartbeat()
# pylint: disable=unused-argument
def on_update(self, event: object) -> None:
"""Ignore primary node status updates.
We listen directly to the Control events on all nodes for this
device.
"""
pass
@property
def value(self) -> object:
"""Get the current value of the device.
Insteon leak sensors set their primary node to On when the state is
DRY, not WET, so we invert the binary state if the user indicates
that it is a moisture sensor.
"""
if self._computed_state is None:
# Do this first so we don't invert None on moisture sensors
return None
if self.device_class == 'moisture':
return not self._computed_state
return self._computed_state
@property
def is_on(self) -> bool:
"""Get whether the ISY994 binary sensor device is on.
Note: This method will return false if the current state is UNKNOWN
"""
return bool(self.value)
@property
def state(self):
"""Return the state of the binary sensor."""
if self._computed_state is None:
return None
return STATE_ON if self.is_on else STATE_OFF
@property
def device_class(self) -> str:
"""Return the class of this device.
This was discovered by parsing the device type code during init
"""
return self._device_class_from_type
class ISYBinarySensorHeartbeat(isy.ISYDevice, BinarySensorDevice):
"""Representation of the battery state of an ISY994 sensor."""
def __init__(self, node, parent_device) -> None:
"""Initialize the ISY994 binary sensor device."""
super().__init__(node)
self._computed_state = None
self._parent_device = parent_device
self._heartbeat_timer = None
@asyncio.coroutine
def async_added_to_hass(self) -> None:
"""Subscribe to the node and subnode event emitters."""
yield from super().async_added_to_hass()
self._node.controlEvents.subscribe(
self._heartbeat_node_control_handler)
# Start the timer on bootup, so we can change from UNKNOWN to ON
self._restart_timer()
def _heartbeat_node_control_handler(self, event: object) -> None:
"""Update the heartbeat timestamp when an On event is sent."""
if event == 'DON':
self.heartbeat()
def heartbeat(self):
"""Mark the device as online, and restart the 25 hour timer.
This gets called when the heartbeat node beats, but also when the
parent sensor sends any events, as we can trust that to mean the device
is online. This mitigates the risk of false positives due to a single
missed heartbeat event.
"""
self._computed_state = False
self._restart_timer()
self.schedule_update_ha_state()
def _restart_timer(self):
"""Restart the 25 hour timer."""
try:
self._heartbeat_timer()
self._heartbeat_timer = None
except TypeError:
# No heartbeat timer is active
pass
# pylint: disable=unused-argument
@callback
def timer_elapsed(now) -> None:
"""Heartbeat missed; set state to indicate dead battery."""
self._computed_state = True
self._heartbeat_timer = None
self.schedule_update_ha_state()
point_in_time = dt_util.utcnow() + timedelta(hours=25)
_LOGGER.debug("Timer starting. Now: %s Then: %s",
dt_util.utcnow(), point_in_time)
self._heartbeat_timer = async_track_point_in_utc_time(
self.hass, timer_elapsed, point_in_time)
# pylint: disable=unused-argument
def on_update(self, event: object) -> None:
"""Ignore node status updates.
We listen directly to the Control events for this device.
"""
pass
@property
def value(self) -> object:
"""Get the current value of this sensor."""
return self._computed_state
@property
def is_on(self) -> bool:
"""Get whether the ISY994 binary sensor device is on.
Note: This method will return false if the current state is UNKNOWN
"""
return bool(self.value)
@property
def state(self):
"""Return the state of the binary sensor."""
if self._computed_state is None:
return None
return STATE_ON if self.is_on else STATE_OFF
@property
def device_class(self) -> str:
"""Get the class of this device."""
return 'battery'
@property
def device_state_attributes(self):
"""Get the state attributes for the device."""
attr = super().device_state_attributes
attr['parent_entity_id'] = self._parent_device.entity_id
return attr
class ISYBinarySensorProgram(isy.ISYDevice, BinarySensorDevice):
"""Representation of an ISY994 binary sensor program.
This does not need all of the subnode logic in the device version of binary
sensors.
"""
def __init__(self, name, node) -> None:
"""Initialize the ISY994 binary sensor program."""
super().__init__(node)
self._name = name
@property @property
def is_on(self) -> bool: def is_on(self) -> bool:
"""Get whether the ISY994 binary sensor device is on.""" """Get whether the ISY994 binary sensor device is on."""
return bool(self.value) return bool(self.value)
class ISYBinarySensorProgram(ISYBinarySensorDevice):
"""Representation of an ISY994 binary sensor program."""
def __init__(self, name, node) -> None:
"""Initialize the ISY994 binary sensor program."""
ISYBinarySensorDevice.__init__(self, node)
self._name = name

View File

@ -9,40 +9,48 @@ import logging
import voluptuous as vol import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.binary_sensor import ( from homeassistant.components.binary_sensor import (
BinarySensorDevice, PLATFORM_SCHEMA, DEVICE_CLASSES_SCHEMA) DEVICE_CLASSES_SCHEMA, PLATFORM_SCHEMA, BinarySensorDevice)
from homeassistant.const import ( from homeassistant.const import (
CONF_NAME, CONF_ENTITY_ID, CONF_TYPE, STATE_UNKNOWN, ATTR_ENTITY_ID, CONF_DEVICE_CLASS, CONF_ENTITY_ID, CONF_NAME,
ATTR_ENTITY_ID, CONF_DEVICE_CLASS) STATE_UNKNOWN)
from homeassistant.core import callback from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.event import async_track_state_change from homeassistant.helpers.event import async_track_state_change
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
ATTR_HYSTERESIS = 'hysteresis' ATTR_HYSTERESIS = 'hysteresis'
ATTR_LOWER = 'lower'
ATTR_POSITION = 'position'
ATTR_SENSOR_VALUE = 'sensor_value' ATTR_SENSOR_VALUE = 'sensor_value'
ATTR_THRESHOLD = 'threshold'
ATTR_TYPE = 'type' ATTR_TYPE = 'type'
ATTR_UPPER = 'upper'
CONF_HYSTERESIS = 'hysteresis' CONF_HYSTERESIS = 'hysteresis'
CONF_LOWER = 'lower' CONF_LOWER = 'lower'
CONF_THRESHOLD = 'threshold'
CONF_UPPER = 'upper' CONF_UPPER = 'upper'
DEFAULT_NAME = 'Threshold' DEFAULT_NAME = 'Threshold'
DEFAULT_HYSTERESIS = 0.0 DEFAULT_HYSTERESIS = 0.0
SENSOR_TYPES = [CONF_LOWER, CONF_UPPER] POSITION_ABOVE = 'above'
POSITION_BELOW = 'below'
POSITION_IN_RANGE = 'in_range'
POSITION_UNKNOWN = 'unknown'
TYPE_LOWER = 'lower'
TYPE_RANGE = 'range'
TYPE_UPPER = 'upper'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Required(CONF_ENTITY_ID): cv.entity_id,
vol.Required(CONF_THRESHOLD): vol.Coerce(float),
vol.Required(CONF_TYPE): vol.In(SENSOR_TYPES),
vol.Optional(
CONF_HYSTERESIS, default=DEFAULT_HYSTERESIS): vol.Coerce(float),
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_HYSTERESIS, default=DEFAULT_HYSTERESIS):
vol.Coerce(float),
vol.Optional(CONF_LOWER): vol.Coerce(float),
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_UPPER): vol.Coerce(float),
}) })
@ -51,47 +59,44 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up the Threshold sensor.""" """Set up the Threshold sensor."""
entity_id = config.get(CONF_ENTITY_ID) entity_id = config.get(CONF_ENTITY_ID)
name = config.get(CONF_NAME) name = config.get(CONF_NAME)
threshold = config.get(CONF_THRESHOLD) lower = config.get(CONF_LOWER)
upper = config.get(CONF_UPPER)
hysteresis = config.get(CONF_HYSTERESIS) hysteresis = config.get(CONF_HYSTERESIS)
limit_type = config.get(CONF_TYPE)
device_class = config.get(CONF_DEVICE_CLASS) device_class = config.get(CONF_DEVICE_CLASS)
async_add_devices([ThresholdSensor( async_add_devices([ThresholdSensor(
hass, entity_id, name, threshold, hass, entity_id, name, lower, upper, hysteresis, device_class)], True)
hysteresis, limit_type, device_class)
], True)
return True
class ThresholdSensor(BinarySensorDevice): class ThresholdSensor(BinarySensorDevice):
"""Representation of a Threshold sensor.""" """Representation of a Threshold sensor."""
def __init__(self, hass, entity_id, name, threshold, def __init__(self, hass, entity_id, name, lower, upper, hysteresis,
hysteresis, limit_type, device_class): device_class):
"""Initialize the Threshold sensor.""" """Initialize the Threshold sensor."""
self._hass = hass self._hass = hass
self._entity_id = entity_id self._entity_id = entity_id
self.is_upper = limit_type == 'upper'
self._name = name self._name = name
self._threshold = threshold self._threshold_lower = lower
self._threshold_upper = upper
self._hysteresis = hysteresis self._hysteresis = hysteresis
self._device_class = device_class self._device_class = device_class
self._state = False
self.sensor_value = 0
@callback self._state_position = None
self._state = False
self.sensor_value = None
# pylint: disable=invalid-name # pylint: disable=invalid-name
@callback
def async_threshold_sensor_state_listener( def async_threshold_sensor_state_listener(
entity, old_state, new_state): entity, old_state, new_state):
"""Handle sensor state changes.""" """Handle sensor state changes."""
if new_state.state == STATE_UNKNOWN:
return
try: try:
self.sensor_value = float(new_state.state) self.sensor_value = None if new_state.state == STATE_UNKNOWN \
except ValueError: else float(new_state.state)
_LOGGER.error("State is not numerical") except (ValueError, TypeError):
self.sensor_value = None
_LOGGER.warning("State is not numerical")
hass.async_add_job(self.async_update_ha_state, True) hass.async_add_job(self.async_update_ha_state, True)
@ -118,23 +123,67 @@ class ThresholdSensor(BinarySensorDevice):
"""Return the sensor class of the sensor.""" """Return the sensor class of the sensor."""
return self._device_class return self._device_class
@property
def threshold_type(self):
"""Return the type of threshold this sensor represents."""
if self._threshold_lower and self._threshold_upper:
return TYPE_RANGE
elif self._threshold_lower:
return TYPE_LOWER
elif self._threshold_upper:
return TYPE_UPPER
@property @property
def device_state_attributes(self): def device_state_attributes(self):
"""Return the state attributes of the sensor.""" """Return the state attributes of the sensor."""
return { return {
ATTR_ENTITY_ID: self._entity_id, ATTR_ENTITY_ID: self._entity_id,
ATTR_SENSOR_VALUE: self.sensor_value,
ATTR_THRESHOLD: self._threshold,
ATTR_HYSTERESIS: self._hysteresis, ATTR_HYSTERESIS: self._hysteresis,
ATTR_TYPE: CONF_UPPER if self.is_upper else CONF_LOWER, ATTR_LOWER: self._threshold_lower,
ATTR_POSITION: self._state_position,
ATTR_SENSOR_VALUE: self.sensor_value,
ATTR_TYPE: self.threshold_type,
ATTR_UPPER: self._threshold_upper,
} }
@asyncio.coroutine @asyncio.coroutine
def async_update(self): def async_update(self):
"""Get the latest data and updates the states.""" """Get the latest data and updates the states."""
if self._hysteresis == 0 and self.sensor_value == self._threshold: def below(threshold):
"""Determine if the sensor value is below a threshold."""
return self.sensor_value < (threshold - self._hysteresis)
def above(threshold):
"""Determine if the sensor value is above a threshold."""
return self.sensor_value > (threshold + self._hysteresis)
if self.sensor_value is None:
self._state_position = POSITION_UNKNOWN
self._state = False self._state = False
elif self.sensor_value > (self._threshold + self._hysteresis):
self._state = self.is_upper elif self.threshold_type == TYPE_LOWER:
elif self.sensor_value < (self._threshold - self._hysteresis): if below(self._threshold_lower):
self._state = not self.is_upper self._state_position = POSITION_BELOW
self._state = True
elif above(self._threshold_lower):
self._state_position = POSITION_ABOVE
self._state = False
elif self.threshold_type == TYPE_UPPER:
if above(self._threshold_upper):
self._state_position = POSITION_ABOVE
self._state = True
elif below(self._threshold_upper):
self._state_position = POSITION_BELOW
self._state = False
elif self.threshold_type == TYPE_RANGE:
if below(self._threshold_lower):
self._state_position = POSITION_BELOW
self._state = False
if above(self._threshold_upper):
self._state_position = POSITION_ABOVE
self._state = False
elif above(self._threshold_lower) and below(self._threshold_upper):
self._state_position = POSITION_IN_RANGE
self._state = True

View File

@ -19,8 +19,8 @@ _LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Perform the setup for Vera controller devices.""" """Perform the setup for Vera controller devices."""
add_devices( add_devices(
VeraBinarySensor(device, VERA_CONTROLLER) VeraBinarySensor(device, hass.data[VERA_CONTROLLER])
for device in VERA_DEVICES['binary_sensor']) for device in hass.data[VERA_DEVICES]['binary_sensor'])
class VeraBinarySensor(VeraDevice, BinarySensorDevice): class VeraBinarySensor(VeraDevice, BinarySensorDevice):

View File

@ -0,0 +1,230 @@
"""
Support for WebDav Calendar.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/calendar.caldav/
"""
import logging
import re
from datetime import datetime, timedelta
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.calendar import (
CalendarEventDevice, PLATFORM_SCHEMA)
from homeassistant.const import (
CONF_NAME, CONF_PASSWORD, CONF_URL, CONF_USERNAME)
from homeassistant.util import dt, Throttle
REQUIREMENTS = ['caldav==0.5.0']
_LOGGER = logging.getLogger(__name__)
CONF_DEVICE_ID = 'device_id'
CONF_CALENDARS = 'calendars'
CONF_CUSTOM_CALENDARS = 'custom_calendars'
CONF_CALENDAR = 'calendar'
CONF_SEARCH = 'search'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
# pylint: disable=no-value-for-parameter
vol.Required(CONF_URL): vol.Url(),
vol.Optional(CONF_CALENDARS, default=[]):
vol.All(cv.ensure_list, vol.Schema([
cv.string
])),
vol.Inclusive(CONF_USERNAME, 'authentication'): cv.string,
vol.Inclusive(CONF_PASSWORD, 'authentication'): cv.string,
vol.Optional(CONF_CUSTOM_CALENDARS, default=[]):
vol.All(cv.ensure_list, vol.Schema([
vol.Schema({
vol.Required(CONF_NAME): cv.string,
vol.Required(CONF_CALENDAR): cv.string,
vol.Required(CONF_SEARCH): cv.string
})
]))
})
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15)
def setup_platform(hass, config, add_devices, disc_info=None):
"""Set up the WebDav Calendar platform."""
import caldav
client = caldav.DAVClient(config.get(CONF_URL),
None,
config.get(CONF_USERNAME),
config.get(CONF_PASSWORD))
# Retrieve all the remote calendars
calendars = client.principal().calendars()
calendar_devices = []
for calendar in list(calendars):
# If a calendar name was given in the configuration,
# ignore all the others
if (config.get(CONF_CALENDARS)
and calendar.name not in config.get(CONF_CALENDARS)):
_LOGGER.debug("Ignoring calendar '%s'", calendar.name)
continue
# Create additional calendars based on custom filtering
# rules
for cust_calendar in config.get(CONF_CUSTOM_CALENDARS):
# Check that the base calendar matches
if cust_calendar.get(CONF_CALENDAR) != calendar.name:
continue
device_data = {
CONF_NAME: cust_calendar.get(CONF_NAME),
CONF_DEVICE_ID: "{} {}".format(
cust_calendar.get(CONF_CALENDAR),
cust_calendar.get(CONF_NAME)),
}
calendar_devices.append(
WebDavCalendarEventDevice(hass,
device_data,
calendar,
True,
cust_calendar.get(CONF_SEARCH))
)
# Create a default calendar if there was no custom one
if not config.get(CONF_CUSTOM_CALENDARS):
device_data = {
CONF_NAME: calendar.name,
CONF_DEVICE_ID: calendar.name
}
calendar_devices.append(
WebDavCalendarEventDevice(hass, device_data, calendar)
)
# Finally add all the calendars we've created
add_devices(calendar_devices)
class WebDavCalendarEventDevice(CalendarEventDevice):
"""A device for getting the next Task from a WebDav Calendar."""
def __init__(self,
hass,
device_data,
calendar,
all_day=False,
search=None):
"""Create the WebDav Calendar Event Device."""
self.data = WebDavCalendarData(calendar, all_day, search)
super().__init__(hass, device_data)
@property
def device_state_attributes(self):
"""Return the device state attributes."""
if self.data.event is None:
# No tasks, we don't REALLY need to show anything.
return {}
attributes = super().device_state_attributes
return attributes
class WebDavCalendarData(object):
"""Class to utilize the calendar dav client object to get next event."""
def __init__(self, calendar, include_all_day, search):
"""Set up how we are going to search the WebDav calendar."""
self.calendar = calendar
self.include_all_day = include_all_day
self.search = search
self.event = None
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
"""Get the latest data."""
# We have to retrieve the results for the whole day as the server
# won't return events that have already started
results = self.calendar.date_search(
dt.start_of_local_day(),
dt.start_of_local_day() + timedelta(days=1)
)
# dtstart can be a date or datetime depending if the event lasts a
# whole day. Convert everything to datetime to be able to sort it
results.sort(key=lambda x: self.to_datetime(
x.instance.vevent.dtstart.value
))
vevent = next((
event.instance.vevent for event in results
if (self.is_matching(event.instance.vevent, self.search)
and (not self.is_all_day(event.instance.vevent)
or self.include_all_day)
and not self.is_over(event.instance.vevent))), None)
# If no matching event could be found
if vevent is None:
_LOGGER.debug(
"No matching event found in the %d results for %s",
len(results),
self.calendar.name,
)
self.event = None
return True
# Populate the entity attributes with the event values
self.event = {
"summary": vevent.summary.value,
"start": self.get_hass_date(vevent.dtstart.value),
"end": self.get_hass_date(vevent.dtend.value),
"location": self.get_attr_value(vevent, "location"),
"description": self.get_attr_value(vevent, "description")
}
return True
@staticmethod
def is_matching(vevent, search):
"""Return if the event matches the filter critera."""
if search is None:
return True
pattern = re.compile(search)
return (hasattr(vevent, "summary")
and pattern.match(vevent.summary.value)
or hasattr(vevent, "location")
and pattern.match(vevent.location.value)
or hasattr(vevent, "description")
and pattern.match(vevent.description.value))
@staticmethod
def is_all_day(vevent):
"""Return if the event last the whole day."""
return not isinstance(vevent.dtstart.value, datetime)
@staticmethod
def is_over(vevent):
"""Return if the event is over."""
return dt.now() > WebDavCalendarData.to_datetime(vevent.dtend.value)
@staticmethod
def get_hass_date(obj):
"""Return if the event matches."""
if isinstance(obj, datetime):
return {"dateTime": obj.isoformat()}
return {"date": obj.isoformat()}
@staticmethod
def to_datetime(obj):
"""Return a datetime."""
if isinstance(obj, datetime):
return obj
return dt.as_local(dt.dt.datetime.combine(obj, dt.dt.time.min))
@staticmethod
def get_attr_value(obj, attribute):
"""Return the value of the attribute if defined."""
if hasattr(obj, attribute):
return getattr(obj, attribute).value
return None

View File

@ -0,0 +1,95 @@
"""
Support for Canary camera.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/camera.canary/
"""
import logging
import requests
from homeassistant.components.camera import Camera
from homeassistant.components.canary import DATA_CANARY, DEFAULT_TIMEOUT
DEPENDENCIES = ['canary']
_LOGGER = logging.getLogger(__name__)
ATTR_MOTION_START_TIME = "motion_start_time"
ATTR_MOTION_END_TIME = "motion_end_time"
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Canary sensors."""
data = hass.data[DATA_CANARY]
devices = []
for location in data.locations:
entries = data.get_motion_entries(location.location_id)
if entries:
devices.append(CanaryCamera(data, location.location_id,
DEFAULT_TIMEOUT))
add_devices(devices, True)
class CanaryCamera(Camera):
"""An implementation of a Canary security camera."""
def __init__(self, data, location_id, timeout):
"""Initialize a Canary security camera."""
super().__init__()
self._data = data
self._location_id = location_id
self._timeout = timeout
self._location = None
self._motion_entry = None
self._image_content = None
def camera_image(self):
"""Update the status of the camera and return bytes of camera image."""
self.update()
return self._image_content
@property
def name(self):
"""Return the name of this device."""
return self._location.name
@property
def is_recording(self):
"""Return true if the device is recording."""
return self._location.is_recording
@property
def device_state_attributes(self):
"""Return device specific state attributes."""
if self._motion_entry is None:
return None
return {
ATTR_MOTION_START_TIME: self._motion_entry.start_time,
ATTR_MOTION_END_TIME: self._motion_entry.end_time,
}
def update(self):
"""Update the status of the camera."""
self._data.update()
self._location = self._data.get_location(self._location_id)
entries = self._data.get_motion_entries(self._location_id)
if entries:
current = entries[0]
previous = self._motion_entry
if previous is None or previous.entry_id != current.entry_id:
self._motion_entry = current
self._image_content = requests.get(
current.thumbnails[0].image_url,
timeout=self._timeout).content
@property
def motion_detection_enabled(self):
"""Return the camera motion detection status."""
return not self._location.is_recording

View File

@ -0,0 +1,117 @@
"""
Support for Canary.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/canary/
"""
import logging
from datetime import timedelta
import voluptuous as vol
from requests import ConnectTimeout, HTTPError
import homeassistant.helpers.config_validation as cv
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_TIMEOUT
from homeassistant.helpers import discovery
from homeassistant.util import Throttle
REQUIREMENTS = ['py-canary==0.2.3']
_LOGGER = logging.getLogger(__name__)
NOTIFICATION_ID = 'canary_notification'
NOTIFICATION_TITLE = 'Canary Setup'
DOMAIN = 'canary'
DATA_CANARY = 'canary'
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30)
DEFAULT_TIMEOUT = 10
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
}),
}, extra=vol.ALLOW_EXTRA)
CANARY_COMPONENTS = [
'alarm_control_panel', 'camera', 'sensor'
]
def setup(hass, config):
"""Set up the Canary component."""
conf = config[DOMAIN]
username = conf.get(CONF_USERNAME)
password = conf.get(CONF_PASSWORD)
timeout = conf.get(CONF_TIMEOUT)
try:
hass.data[DATA_CANARY] = CanaryData(username, password, timeout)
except (ConnectTimeout, HTTPError) as ex:
_LOGGER.error("Unable to connect to Canary service: %s", str(ex))
hass.components.persistent_notification.create(
'Error: {}<br />'
'You will need to restart hass after fixing.'
''.format(ex),
title=NOTIFICATION_TITLE,
notification_id=NOTIFICATION_ID)
return False
for component in CANARY_COMPONENTS:
discovery.load_platform(hass, component, DOMAIN, {}, config)
return True
class CanaryData(object):
"""Get the latest data and update the states."""
def __init__(self, username, password, timeout):
"""Init the Canary data object."""
from canary.api import Api
self._api = Api(username, password, timeout)
self._locations_by_id = {}
self._readings_by_device_id = {}
self._entries_by_location_id = {}
self.update()
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self, **kwargs):
"""Get the latest data from py-canary."""
for location in self._api.get_locations():
location_id = location.location_id
self._locations_by_id[location_id] = location
self._entries_by_location_id[location_id] = self._api.get_entries(
location_id, entry_type="motion", limit=1)
for device in location.devices:
if device.is_online:
self._readings_by_device_id[device.device_id] = \
self._api.get_latest_readings(device.device_id)
@property
def locations(self):
"""Return a list of locations."""
return self._locations_by_id.values()
def get_motion_entries(self, location_id):
"""Return a list of motion entries based on location_id."""
return self._entries_by_location_id.get(location_id, [])
def get_location(self, location_id):
"""Return a location based on location_id."""
return self._locations_by_id.get(location_id, [])
def get_readings(self, device_id):
"""Return a list of readings based on device_id."""
return self._readings_by_device_id.get(device_id, [])
def set_location_mode(self, location_id, mode_name, is_private=False):
"""Set location mode."""
self._api.set_location_mode(location_id, mode_name, is_private)
self.update(no_throttle=True)

View File

@ -6,7 +6,7 @@ https://home-assistant.io/components/climate.hive/
""" """
from homeassistant.components.climate import ( from homeassistant.components.climate import (
ClimateDevice, STATE_AUTO, STATE_HEAT, STATE_OFF, STATE_ON, ClimateDevice, STATE_AUTO, STATE_HEAT, STATE_OFF, STATE_ON,
SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE) SUPPORT_AUX_HEAT, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE)
from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS
from homeassistant.components.hive import DATA_HIVE from homeassistant.components.hive import DATA_HIVE
@ -16,7 +16,9 @@ HIVE_TO_HASS_STATE = {'SCHEDULE': STATE_AUTO, 'MANUAL': STATE_HEAT,
HASS_TO_HIVE_STATE = {STATE_AUTO: 'SCHEDULE', STATE_HEAT: 'MANUAL', HASS_TO_HIVE_STATE = {STATE_AUTO: 'SCHEDULE', STATE_HEAT: 'MANUAL',
STATE_ON: 'ON', STATE_OFF: 'OFF'} STATE_ON: 'ON', STATE_OFF: 'OFF'}
SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE |
SUPPORT_OPERATION_MODE |
SUPPORT_AUX_HEAT)
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
@ -134,6 +136,43 @@ class HiveClimateEntity(ClimateDevice):
for entity in self.session.entities: for entity in self.session.entities:
entity.handle_update(self.data_updatesource) entity.handle_update(self.data_updatesource)
@property
def is_aux_heat_on(self):
"""Return true if auxiliary heater is on."""
boost_status = None
if self.device_type == "Heating":
boost_status = self.session.heating.get_boost(self.node_id)
elif self.device_type == "HotWater":
boost_status = self.session.hotwater.get_boost(self.node_id)
return boost_status == "ON"
def turn_aux_heat_on(self):
"""Turn auxiliary heater on."""
target_boost_time = 30
if self.device_type == "Heating":
curtemp = self.session.heating.current_temperature(self.node_id)
curtemp = round(curtemp * 2) / 2
target_boost_temperature = curtemp + 0.5
self.session.heating.turn_boost_on(self.node_id,
target_boost_time,
target_boost_temperature)
elif self.device_type == "HotWater":
self.session.hotwater.turn_boost_on(self.node_id,
target_boost_time)
for entity in self.session.entities:
entity.handle_update(self.data_updatesource)
def turn_aux_heat_off(self):
"""Turn auxiliary heater off."""
if self.device_type == "Heating":
self.session.heating.turn_boost_off(self.node_id)
elif self.device_type == "HotWater":
self.session.hotwater.turn_boost_off(self.node_id)
for entity in self.session.entities:
entity.handle_update(self.data_updatesource)
def update(self): def update(self):
"""Update all Node data frome Hive.""" """Update all Node data frome Hive."""
self.session.core.update_data(self.node_id) self.session.core.update_data(self.node_id)

View File

@ -13,11 +13,12 @@ import async_timeout
import voluptuous as vol import voluptuous as vol
from homeassistant.const import ( from homeassistant.const import (
ATTR_TEMPERATURE, CONF_API_KEY, CONF_ID, TEMP_CELSIUS, TEMP_FAHRENHEIT) ATTR_TEMPERATURE, CONF_API_KEY, CONF_ID, STATE_OFF, TEMP_CELSIUS,
TEMP_FAHRENHEIT)
from homeassistant.components.climate import ( from homeassistant.components.climate import (
ATTR_CURRENT_HUMIDITY, ClimateDevice, PLATFORM_SCHEMA, ATTR_CURRENT_HUMIDITY, ClimateDevice, PLATFORM_SCHEMA,
SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE,
SUPPORT_FAN_MODE, SUPPORT_AWAY_MODE, SUPPORT_SWING_MODE, SUPPORT_FAN_MODE, SUPPORT_SWING_MODE,
SUPPORT_AUX_HEAT) SUPPORT_AUX_HEAT)
from homeassistant.exceptions import PlatformNotReady from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
@ -41,9 +42,13 @@ _FETCH_FIELDS = ','.join([
'acState', 'connectionStatus{isAlive}', 'temperatureUnit']) 'acState', 'connectionStatus{isAlive}', 'temperatureUnit'])
_INITIAL_FETCH_FIELDS = 'id,' + _FETCH_FIELDS _INITIAL_FETCH_FIELDS = 'id,' + _FETCH_FIELDS
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE | FIELD_TO_FLAG = {
SUPPORT_FAN_MODE | SUPPORT_AWAY_MODE | SUPPORT_SWING_MODE | 'fanLevel': SUPPORT_FAN_MODE,
SUPPORT_AUX_HEAT) 'mode': SUPPORT_OPERATION_MODE,
'swing': SUPPORT_SWING_MODE,
'targetTemperature': SUPPORT_TARGET_TEMPERATURE,
'on': SUPPORT_AUX_HEAT,
}
@asyncio.coroutine @asyncio.coroutine
@ -85,7 +90,14 @@ class SensiboClimate(ClimateDevice):
@property @property
def supported_features(self): def supported_features(self):
"""Return the list of supported features.""" """Return the list of supported features."""
return SUPPORT_FLAGS return self._supported_features
@property
def state(self):
"""Return the current state."""
if not self.is_aux_heat_on:
return STATE_OFF
return super().state
def _do_update(self, data): def _do_update(self, data):
self._name = data['room']['name'] self._name = data['room']['name']
@ -106,6 +118,10 @@ class SensiboClimate(ClimateDevice):
else: else:
self._temperature_unit = self.unit_of_measurement self._temperature_unit = self.unit_of_measurement
self._temperatures_list = [] self._temperatures_list = []
self._supported_features = 0
for key in self._ac_states:
if key in FIELD_TO_FLAG:
self._supported_features |= FIELD_TO_FLAG[key]
@property @property
def device_state_attributes(self): def device_state_attributes(self):
@ -196,13 +212,13 @@ class SensiboClimate(ClimateDevice):
def min_temp(self): def min_temp(self):
"""Return the minimum temperature.""" """Return the minimum temperature."""
return self._temperatures_list[0] \ return self._temperatures_list[0] \
if len(self._temperatures_list) else super.min_temp() if len(self._temperatures_list) else super().min_temp()
@property @property
def max_temp(self): def max_temp(self):
"""Return the maximum temperature.""" """Return the maximum temperature."""
return self._temperatures_list[-1] \ return self._temperatures_list[-1] \
if len(self._temperatures_list) else super.max_temp() if len(self._temperatures_list) else super().max_temp()
@asyncio.coroutine @asyncio.coroutine
def async_set_temperature(self, **kwargs): def async_set_temperature(self, **kwargs):

View File

@ -32,8 +32,8 @@ SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE |
def setup_platform(hass, config, add_devices_callback, discovery_info=None): def setup_platform(hass, config, add_devices_callback, discovery_info=None):
"""Set up of Vera thermostats.""" """Set up of Vera thermostats."""
add_devices_callback( add_devices_callback(
VeraThermostat(device, VERA_CONTROLLER) for VeraThermostat(device, hass.data[VERA_CONTROLLER]) for
device in VERA_DEVICES['climate']) device in hass.data[VERA_DEVICES]['climate'])
class VeraThermostat(VeraDevice, ClimateDevice): class VeraThermostat(VeraDevice, ClimateDevice):

View File

@ -16,7 +16,7 @@ from homeassistant.components.alexa import smart_home
from . import http_api, iot from . import http_api, iot
from .const import CONFIG_DIR, DOMAIN, SERVERS from .const import CONFIG_DIR, DOMAIN, SERVERS
REQUIREMENTS = ['warrant==0.5.0'] REQUIREMENTS = ['warrant==0.6.1']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -27,7 +27,7 @@ CONF_RELAYER = 'relayer'
CONF_USER_POOL_ID = 'user_pool_id' CONF_USER_POOL_ID = 'user_pool_id'
MODE_DEV = 'development' MODE_DEV = 'development'
DEFAULT_MODE = MODE_DEV DEFAULT_MODE = 'production'
DEPENDENCIES = ['http'] DEPENDENCIES = ['http']
ALEXA_SCHEMA = vol.Schema({ ALEXA_SCHEMA = vol.Schema({
@ -42,10 +42,10 @@ CONFIG_SCHEMA = vol.Schema({
vol.Optional(CONF_MODE, default=DEFAULT_MODE): vol.Optional(CONF_MODE, default=DEFAULT_MODE):
vol.In([MODE_DEV] + list(SERVERS)), vol.In([MODE_DEV] + list(SERVERS)),
# Change to optional when we include real servers # Change to optional when we include real servers
vol.Required(CONF_COGNITO_CLIENT_ID): str, vol.Optional(CONF_COGNITO_CLIENT_ID): str,
vol.Required(CONF_USER_POOL_ID): str, vol.Optional(CONF_USER_POOL_ID): str,
vol.Required(CONF_REGION): str, vol.Optional(CONF_REGION): str,
vol.Required(CONF_RELAYER): str, vol.Optional(CONF_RELAYER): str,
vol.Optional(CONF_ALEXA): ALEXA_SCHEMA vol.Optional(CONF_ALEXA): ALEXA_SCHEMA
}), }),
}, extra=vol.ALLOW_EXTRA) }, extra=vol.ALLOW_EXTRA)
@ -117,10 +117,6 @@ class Cloud:
@property @property
def subscription_expired(self): def subscription_expired(self):
"""Return a boolen if the subscription has expired.""" """Return a boolen if the subscription has expired."""
# For now, don't enforce subscriptions to exist
if 'custom:sub-exp' not in self.claims:
return False
return dt_util.utcnow() > self.expiration_date return dt_util.utcnow() > self.expiration_date
@property @property

View File

@ -68,11 +68,14 @@ def register(cloud, email, password):
from botocore.exceptions import ClientError from botocore.exceptions import ClientError
cognito = _cognito(cloud) cognito = _cognito(cloud)
# Workaround for bug in Warrant. PR with fix:
# https://github.com/capless/warrant/pull/82
cognito.add_base_attributes()
try: try:
if cloud.cognito_email_based: if cloud.cognito_email_based:
cognito.register(email, password, email=email) cognito.register(email, password)
else: else:
cognito.register(_generate_username(email), password, email=email) cognito.register(_generate_username(email), password)
except ClientError as err: except ClientError as err:
raise _map_aws_exception(err) raise _map_aws_exception(err)

View File

@ -4,13 +4,12 @@ CONFIG_DIR = '.cloud'
REQUEST_TIMEOUT = 10 REQUEST_TIMEOUT = 10
SERVERS = { SERVERS = {
# Example entry: 'production': {
# 'production': { 'cognito_client_id': '60i2uvhvbiref2mftj7rgcrt9u',
# 'cognito_client_id': '', 'user_pool_id': 'us-east-1_87ll5WOP8',
# 'user_pool_id': '', 'region': 'us-east-1',
# 'region': '', 'relayer': 'wss://cloud.hass.io:8000/websocket'
# 'relayer': '' }
# }
} }
MESSAGE_EXPIRATION = """ MESSAGE_EXPIRATION = """

View File

@ -1,4 +1,4 @@
"""Provide configuration end points for Z-Wave.""" """Provide configuration end points for Automations."""
import asyncio import asyncio
from homeassistant.components.config import EditIdBasedConfigView from homeassistant.components.config import EditIdBasedConfigView

View File

@ -69,7 +69,10 @@ class ISYCoverDevice(isy.ISYDevice, CoverDevice):
@property @property
def state(self) -> str: def state(self) -> str:
"""Get the state of the ISY994 cover device.""" """Get the state of the ISY994 cover device."""
return VALUE_TO_STATE.get(self.value, STATE_OPEN) if self.is_unknown():
return None
else:
return VALUE_TO_STATE.get(self.value, STATE_OPEN)
def open_cover(self, **kwargs) -> None: def open_cover(self, **kwargs) -> None:
"""Send the open cover command to the ISY994 cover device.""" """Send the open cover command to the ISY994 cover device."""

View File

@ -0,0 +1,65 @@
"""
Support for Tellstick covers.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/cover.tellstick/
"""
from homeassistant.components.cover import CoverDevice
from homeassistant.components.tellstick import (
DEFAULT_SIGNAL_REPETITIONS, ATTR_DISCOVER_DEVICES, ATTR_DISCOVER_CONFIG,
DATA_TELLSTICK, TellstickDevice)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Tellstick covers."""
if (discovery_info is None or
discovery_info[ATTR_DISCOVER_DEVICES] is None):
return
signal_repetitions = discovery_info.get(
ATTR_DISCOVER_CONFIG, DEFAULT_SIGNAL_REPETITIONS)
add_devices([TellstickCover(hass.data[DATA_TELLSTICK][tellcore_id],
signal_repetitions)
for tellcore_id in discovery_info[ATTR_DISCOVER_DEVICES]],
True)
class TellstickCover(TellstickDevice, CoverDevice):
"""Representation of a Tellstick cover."""
@property
def is_closed(self):
"""Return the current position of the cover is not possible."""
return None
@property
def assumed_state(self):
"""Return True if unable to access real state of the entity."""
return True
def close_cover(self, **kwargs):
"""Close the cover."""
self._tellcore_device.down()
def open_cover(self, **kwargs):
"""Open the cover."""
self._tellcore_device.up()
def stop_cover(self, **kwargs):
"""Stop the cover."""
self._tellcore_device.stop()
def _parse_tellcore_data(self, tellcore_data):
"""Turn the value received from tellcore into something useful."""
pass
def _parse_ha_data(self, kwargs):
"""Turn the value from HA into something useful."""
pass
def _update_model(self, new_state, data):
"""Update the device entity state to match the arguments."""
pass

View File

@ -18,8 +18,8 @@ _LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Vera covers.""" """Set up the Vera covers."""
add_devices( add_devices(
VeraCover(device, VERA_CONTROLLER) for VeraCover(device, hass.data[VERA_CONTROLLER]) for
device in VERA_DEVICES['cover']) device in hass.data[VERA_DEVICES]['cover'])
class VeraCover(VeraDevice, CoverDevice): class VeraCover(VeraDevice, CoverDevice):

View File

@ -67,6 +67,15 @@ _IP_NEIGH_REGEX = re.compile(
r'\s?(router)?' r'\s?(router)?'
r'(?P<status>(\w+))') r'(?P<status>(\w+))')
_ARP_CMD = 'arp -n'
_ARP_REGEX = re.compile(
r'.+\s' +
r'\((?P<ip>([0-9]{1,3}[\.]){3}[0-9]{1,3})\)\s' +
r'.+\s' +
r'(?P<mac>(([0-9a-f]{2}[:-]){5}([0-9a-f]{2})))' +
r'\s' +
r'.*')
# pylint: disable=unused-argument # pylint: disable=unused-argument
def get_scanner(hass, config): def get_scanner(hass, config):
@ -76,7 +85,22 @@ def get_scanner(hass, config):
return scanner if scanner.success_init else None return scanner if scanner.success_init else None
AsusWrtResult = namedtuple('AsusWrtResult', 'neighbors leases') def _parse_lines(lines, regex):
"""Parse the lines using the given regular expression.
If a line can't be parsed it is logged and skipped in the output.
"""
results = []
for line in lines:
match = regex.search(line)
if not match:
_LOGGER.debug("Could not parse row: %s", line)
continue
results.append(match.groupdict())
return results
Device = namedtuple('Device', ['mac', 'ip', 'name'])
class AsusWrtDeviceScanner(DeviceScanner): class AsusWrtDeviceScanner(DeviceScanner):
@ -121,16 +145,13 @@ class AsusWrtDeviceScanner(DeviceScanner):
def scan_devices(self): def scan_devices(self):
"""Scan for new devices and return a list with found device IDs.""" """Scan for new devices and return a list with found device IDs."""
self._update_info() self._update_info()
return [client['mac'] for client in self.last_results] return list(self.last_results.keys())
def get_device_name(self, device): def get_device_name(self, device):
"""Return the name of the given device or None if we don't know.""" """Return the name of the given device or None if we don't know."""
if not self.last_results: if device not in self.last_results:
return None return None
for client in self.last_results: return self.last_results[device].name
if client['mac'] == device:
return client['host']
return None
def _update_info(self): def _update_info(self):
"""Ensure the information from the ASUSWRT router is up to date. """Ensure the information from the ASUSWRT router is up to date.
@ -145,72 +166,71 @@ class AsusWrtDeviceScanner(DeviceScanner):
if not data: if not data:
return False return False
active_clients = [client for client in data.values() if self.last_results = data
client['status'] == 'REACHABLE' or
client['status'] == 'DELAY' or
client['status'] == 'STALE' or
client['status'] == 'IN_ASSOCLIST']
self.last_results = active_clients
return True return True
def get_asuswrt_data(self): def get_asuswrt_data(self):
"""Retrieve data from ASUSWRT and return parsed result.""" """Retrieve data from ASUSWRT.
result = self.connection.get_result()
if not result:
return {}
Calls various commands on the router and returns the superset of all
responses. Some commands will not work on some routers.
"""
devices = {} devices = {}
if self.mode == 'ap': devices.update(self._get_wl())
for lease in result.leases: devices.update(self._get_arp())
match = _WL_REGEX.search(lease.decode('utf-8')) devices.update(self._get_neigh())
if not self.mode == 'ap':
devices.update(self._get_leases())
return devices
if not match: def _get_wl(self):
_LOGGER.warning("Could not parse wl row: %s", lease) lines = self.connection.run_command(_WL_CMD)
continue if not lines:
return {}
result = _parse_lines(lines, _WL_REGEX)
devices = {}
for device in result:
mac = device['mac'].upper()
devices[mac] = Device(mac, None, None)
return devices
def _get_leases(self):
lines = self.connection.run_command(_LEASES_CMD)
if not lines:
return {}
lines = [line for line in lines if not line.startswith('duid ')]
result = _parse_lines(lines, _LEASES_REGEX)
devices = {}
for device in result:
# For leases where the client doesn't set a hostname, ensure it
# is blank and not '*', which breaks entity_id down the line.
host = device['host']
if host == '*':
host = '' host = ''
mac = device['mac'].upper()
devices[mac] = Device(mac, device['ip'], host)
return devices
devices[match.group('mac').upper()] = { def _get_neigh(self):
'host': host, lines = self.connection.run_command(_IP_NEIGH_CMD)
'status': 'IN_ASSOCLIST', if not lines:
'ip': '', return {}
'mac': match.group('mac').upper(), result = _parse_lines(lines, _IP_NEIGH_REGEX)
} devices = {}
for device in result:
else: mac = device['mac'].upper()
for lease in result.leases: devices[mac] = Device(mac, None, None)
if lease.startswith(b'duid '): return devices
continue
match = _LEASES_REGEX.search(lease.decode('utf-8'))
if not match:
_LOGGER.warning("Could not parse lease row: %s", lease)
continue
# For leases where the client doesn't set a hostname, ensure it
# is blank and not '*', which breaks entity_id down the line.
host = match.group('host')
if host == '*':
host = ''
devices[match.group('mac')] = {
'host': host,
'status': '',
'ip': match.group('ip'),
'mac': match.group('mac').upper(),
}
for neighbor in result.neighbors:
match = _IP_NEIGH_REGEX.search(neighbor.decode('utf-8'))
if not match:
_LOGGER.warning("Could not parse neighbor row: %s",
neighbor)
continue
if match.group('mac') in devices:
devices[match.group('mac')]['status'] = (
match.group('status'))
def _get_arp(self):
lines = self.connection.run_command(_ARP_CMD)
if not lines:
return {}
result = _parse_lines(lines, _ARP_REGEX)
devices = {}
for device in result:
mac = device['mac'].upper()
devices[mac] = Device(mac, device['ip'], None)
return devices return devices
@ -247,8 +267,8 @@ class SshConnection(_Connection):
self._ssh_key = ssh_key self._ssh_key = ssh_key
self._ap = ap self._ap = ap
def get_result(self): def run_command(self, command):
"""Retrieve a single AsusWrtResult through an SSH connection. """Run commands through an SSH connection.
Connect to the SSH server if not currently connected, otherwise Connect to the SSH server if not currently connected, otherwise
use the existing connection. use the existing connection.
@ -258,19 +278,10 @@ class SshConnection(_Connection):
try: try:
if not self.connected: if not self.connected:
self.connect() self.connect()
if self._ap: self._ssh.sendline(command)
neighbors = [''] self._ssh.prompt()
self._ssh.sendline(_WL_CMD) lines = self._ssh.before.split(b'\n')[1:-1]
self._ssh.prompt() return [line.decode('utf-8') for line in lines]
leases_result = self._ssh.before.split(b'\n')[1:-1]
else:
self._ssh.sendline(_IP_NEIGH_CMD)
self._ssh.prompt()
neighbors = self._ssh.before.split(b'\n')[1:-1]
self._ssh.sendline(_LEASES_CMD)
self._ssh.prompt()
leases_result = self._ssh.before.split(b'\n')[1:-1]
return AsusWrtResult(neighbors, leases_result)
except exceptions.EOF as err: except exceptions.EOF as err:
_LOGGER.error("Connection refused. SSH enabled?") _LOGGER.error("Connection refused. SSH enabled?")
self.disconnect() self.disconnect()
@ -326,8 +337,8 @@ class TelnetConnection(_Connection):
self._ap = ap self._ap = ap
self._prompt_string = None self._prompt_string = None
def get_result(self): def run_command(self, command):
"""Retrieve a single AsusWrtResult through a Telnet connection. """Run a command through a Telnet connection.
Connect to the Telnet server if not currently connected, otherwise Connect to the Telnet server if not currently connected, otherwise
use the existing connection. use the existing connection.
@ -336,18 +347,9 @@ class TelnetConnection(_Connection):
if not self.connected: if not self.connected:
self.connect() self.connect()
self._telnet.write('{}\n'.format(_IP_NEIGH_CMD).encode('ascii')) self._telnet.write('{}\n'.format(command).encode('ascii'))
neighbors = (self._telnet.read_until(self._prompt_string). return (self._telnet.read_until(self._prompt_string).
split(b'\n')[1:-1]) split(b'\n')[1:-1])
if self._ap:
self._telnet.write('{}\n'.format(_WL_CMD).encode('ascii'))
leases_result = (self._telnet.read_until(self._prompt_string).
split(b'\n')[1:-1])
else:
self._telnet.write('{}\n'.format(_LEASES_CMD).encode('ascii'))
leases_result = (self._telnet.read_until(self._prompt_string).
split(b'\n')[1:-1])
return AsusWrtResult(neighbors, leases_result)
except EOFError: except EOFError:
_LOGGER.error("Unexpected response from router") _LOGGER.error("Unexpected response from router")
self.disconnect() self.disconnect()

View File

@ -5,23 +5,37 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.gpslogger/ https://home-assistant.io/components/device_tracker.gpslogger/
""" """
import asyncio import asyncio
from functools import partial
import logging import logging
from hmac import compare_digest
from homeassistant.const import HTTP_UNPROCESSABLE_ENTITY from aiohttp.web import Request, HTTPUnauthorized # NOQA
from homeassistant.components.http import HomeAssistantView import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.const import (
CONF_PASSWORD, HTTP_UNPROCESSABLE_ENTITY
)
from homeassistant.components.http import (
CONF_API_PASSWORD, HomeAssistantView
)
# pylint: disable=unused-import # pylint: disable=unused-import
from homeassistant.components.device_tracker import ( # NOQA from homeassistant.components.device_tracker import ( # NOQA
DOMAIN, PLATFORM_SCHEMA) DOMAIN, PLATFORM_SCHEMA
)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['http'] DEPENDENCIES = ['http']
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_PASSWORD): cv.string,
})
def setup_scanner(hass, config, see, discovery_info=None):
@asyncio.coroutine
def async_setup_scanner(hass, config, async_see, discovery_info=None):
"""Set up an endpoint for the GPSLogger application.""" """Set up an endpoint for the GPSLogger application."""
hass.http.register_view(GPSLoggerView(see)) hass.http.register_view(GPSLoggerView(async_see, config))
return True return True
@ -32,26 +46,36 @@ class GPSLoggerView(HomeAssistantView):
url = '/api/gpslogger' url = '/api/gpslogger'
name = 'api:gpslogger' name = 'api:gpslogger'
def __init__(self, see): def __init__(self, async_see, config):
"""Initialize GPSLogger url endpoints.""" """Initialize GPSLogger url endpoints."""
self.see = see self.async_see = async_see
self._password = config.get(CONF_PASSWORD)
# this component does not require external authentication if
# password is set
self.requires_auth = self._password is None
@asyncio.coroutine @asyncio.coroutine
def get(self, request): def get(self, request: Request):
"""Handle for GPSLogger message received as GET.""" """Handle for GPSLogger message received as GET."""
res = yield from self._handle(request.app['hass'], request.query) hass = request.app['hass']
return res data = request.query
if self._password is not None:
authenticated = CONF_API_PASSWORD in data and compare_digest(
self._password,
data[CONF_API_PASSWORD]
)
if not authenticated:
raise HTTPUnauthorized()
@asyncio.coroutine
def _handle(self, hass, data):
"""Handle GPSLogger requests."""
if 'latitude' not in data or 'longitude' not in data: if 'latitude' not in data or 'longitude' not in data:
return ('Latitude and longitude not specified.', return ('Latitude and longitude not specified.',
HTTP_UNPROCESSABLE_ENTITY) HTTP_UNPROCESSABLE_ENTITY)
if 'device' not in data: if 'device' not in data:
_LOGGER.error("Device id not specified") _LOGGER.error("Device id not specified")
return ('Device id not specified.', HTTP_UNPROCESSABLE_ENTITY) return ('Device id not specified.',
HTTP_UNPROCESSABLE_ENTITY)
device = data['device'].replace('-', '') device = data['device'].replace('-', '')
gps_location = (data['latitude'], data['longitude']) gps_location = (data['latitude'], data['longitude'])
@ -75,10 +99,11 @@ class GPSLoggerView(HomeAssistantView):
if 'activity' in data: if 'activity' in data:
attrs['activity'] = data['activity'] attrs['activity'] = data['activity']
yield from hass.async_add_job( hass.async_add_job(self.async_see(
partial(self.see, dev_id=device, dev_id=device,
gps=gps_location, battery=battery, gps=gps_location, battery=battery,
gps_accuracy=accuracy, gps_accuracy=accuracy,
attributes=attrs)) attributes=attrs
))
return 'Setting location for {}'.format(device) return 'Setting location for {}'.format(device)

View File

@ -94,8 +94,26 @@ class MerakiView(HomeAssistantView):
def _handle(self, hass, data): def _handle(self, hass, data):
for i in data["data"]["observations"]: for i in data["data"]["observations"]:
data["data"]["secret"] = "hidden" data["data"]["secret"] = "hidden"
lat = i["location"]["lat"]
lng = i["location"]["lng"]
try:
accuracy = int(float(i["location"]["unc"]))
except ValueError:
accuracy = 0
mac = i["clientMac"] mac = i["clientMac"]
_LOGGER.debug("clientMac: %s", mac) _LOGGER.debug("clientMac: %s", mac)
if lat == "NaN" or lng == "NaN":
_LOGGER.debug(
"No coordinates received, skipping location for: " + mac
)
gps_location = None
accuracy = None
else:
gps_location = (lat, lng)
attrs = {} attrs = {}
if i.get('os', False): if i.get('os', False):
attrs['os'] = i['os'] attrs['os'] = i['os']
@ -110,7 +128,9 @@ class MerakiView(HomeAssistantView):
if i.get('ssid', False): if i.get('ssid', False):
attrs['ssid'] = i['ssid'] attrs['ssid'] = i['ssid']
hass.async_add_job(self.async_see( hass.async_add_job(self.async_see(
gps=gps_location,
mac=mac, mac=mac,
source_type=SOURCE_TYPE_ROUTER, source_type=SOURCE_TYPE_ROUTER,
gps_accuracy=accuracy,
attributes=attrs attributes=attrs
)) ))

View File

@ -19,7 +19,7 @@ from homeassistant.util.json import load_json, save_json
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['pytile==1.0.0'] REQUIREMENTS = ['pytile==1.1.0']
CLIENT_UUID_CONFIG_FILE = '.tile.conf' CLIENT_UUID_CONFIG_FILE = '.tile.conf'
DEFAULT_ICON = 'mdi:bluetooth' DEFAULT_ICON = 'mdi:bluetooth'
@ -29,14 +29,15 @@ ATTR_ALTITUDE = 'altitude'
ATTR_CONNECTION_STATE = 'connection_state' ATTR_CONNECTION_STATE = 'connection_state'
ATTR_IS_DEAD = 'is_dead' ATTR_IS_DEAD = 'is_dead'
ATTR_IS_LOST = 'is_lost' ATTR_IS_LOST = 'is_lost'
ATTR_LAST_SEEN = 'last_seen'
ATTR_LAST_UPDATED = 'last_updated'
ATTR_RING_STATE = 'ring_state' ATTR_RING_STATE = 'ring_state'
ATTR_VOIP_STATE = 'voip_state' ATTR_VOIP_STATE = 'voip_state'
CONF_SHOW_INACTIVE = 'show_inactive'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string, vol.Required(CONF_PASSWORD): cv.string,
vol.Optional(CONF_SHOW_INACTIVE, default=False): cv.boolean,
vol.Optional(CONF_MONITORED_VARIABLES): vol.Optional(CONF_MONITORED_VARIABLES):
vol.All(cv.ensure_list, [vol.In(DEVICE_TYPES)]), vol.All(cv.ensure_list, [vol.In(DEVICE_TYPES)]),
}) })
@ -79,6 +80,7 @@ class TileDeviceScanner(DeviceScanner):
_LOGGER.debug('Client UUID: %s', self._client.client_uuid) _LOGGER.debug('Client UUID: %s', self._client.client_uuid)
_LOGGER.debug('User UUID: %s', self._client.user_uuid) _LOGGER.debug('User UUID: %s', self._client.user_uuid)
self._show_inactive = config.get(CONF_SHOW_INACTIVE)
self._types = config.get(CONF_MONITORED_VARIABLES) self._types = config.get(CONF_MONITORED_VARIABLES)
self.devices = {} self.devices = {}
@ -91,29 +93,25 @@ class TileDeviceScanner(DeviceScanner):
def _update_info(self, now=None) -> None: def _update_info(self, now=None) -> None:
"""Update the device info.""" """Update the device info."""
device_data = self._client.get_tiles(type_whitelist=self._types) self.devices = self._client.get_tiles(
type_whitelist=self._types, show_inactive=self._show_inactive)
try: if not self.devices:
self.devices = device_data['result']
except KeyError:
_LOGGER.warning('No Tiles found') _LOGGER.warning('No Tiles found')
_LOGGER.debug(device_data)
return return
for info in self.devices.values(): for dev in self.devices:
dev_id = 'tile_{0}'.format(slugify(info['name'])) dev_id = 'tile_{0}'.format(slugify(dev['name']))
lat = info['tileState']['latitude'] lat = dev['tileState']['latitude']
lon = info['tileState']['longitude'] lon = dev['tileState']['longitude']
attrs = { attrs = {
ATTR_ALTITUDE: info['tileState']['altitude'], ATTR_ALTITUDE: dev['tileState']['altitude'],
ATTR_CONNECTION_STATE: info['tileState']['connection_state'], ATTR_CONNECTION_STATE: dev['tileState']['connection_state'],
ATTR_IS_DEAD: info['is_dead'], ATTR_IS_DEAD: dev['is_dead'],
ATTR_IS_LOST: info['tileState']['is_lost'], ATTR_IS_LOST: dev['tileState']['is_lost'],
ATTR_LAST_SEEN: info['tileState']['timestamp'], ATTR_RING_STATE: dev['tileState']['ring_state'],
ATTR_LAST_UPDATED: device_data['timestamp_ms'], ATTR_VOIP_STATE: dev['tileState']['voip_state'],
ATTR_RING_STATE: info['tileState']['ring_state'],
ATTR_VOIP_STATE: info['tileState']['voip_state'],
} }
self.see( self.see(

View File

@ -36,6 +36,7 @@ SERVICE_APPLE_TV = 'apple_tv'
SERVICE_WINK = 'wink' SERVICE_WINK = 'wink'
SERVICE_XIAOMI_GW = 'xiaomi_gw' SERVICE_XIAOMI_GW = 'xiaomi_gw'
SERVICE_TELLDUSLIVE = 'tellstick' SERVICE_TELLDUSLIVE = 'tellstick'
SERVICE_HUE = 'philips_hue'
SERVICE_HANDLERS = { SERVICE_HANDLERS = {
SERVICE_HASS_IOS_APP: ('ios', None), SERVICE_HASS_IOS_APP: ('ios', None),
@ -48,7 +49,7 @@ SERVICE_HANDLERS = {
SERVICE_WINK: ('wink', None), SERVICE_WINK: ('wink', None),
SERVICE_XIAOMI_GW: ('xiaomi_aqara', None), SERVICE_XIAOMI_GW: ('xiaomi_aqara', None),
SERVICE_TELLDUSLIVE: ('tellduslive', None), SERVICE_TELLDUSLIVE: ('tellduslive', None),
'philips_hue': ('light', 'hue'), SERVICE_HUE: ('hue', None),
'google_cast': ('media_player', 'cast'), 'google_cast': ('media_player', 'cast'),
'panasonic_viera': ('media_player', 'panasonic_viera'), 'panasonic_viera': ('media_player', 'panasonic_viera'),
'plex_mediaserver': ('media_player', 'plex'), 'plex_mediaserver': ('media_player', 'plex'),

View File

@ -31,7 +31,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
}) })
REQUIREMENTS = ['python-miio==0.3.2'] REQUIREMENTS = ['python-miio==0.3.3']
ATTR_TEMPERATURE = 'temperature' ATTR_TEMPERATURE = 'temperature'
ATTR_HUMIDITY = 'humidity' ATTR_HUMIDITY = 'humidity'

View File

@ -23,7 +23,7 @@ from homeassistant.const import CONF_NAME, EVENT_THEMES_UPDATED
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.loader import bind_hass from homeassistant.loader import bind_hass
REQUIREMENTS = ['home-assistant-frontend==20171206.0', 'user-agents==1.1.0'] REQUIREMENTS = ['home-assistant-frontend==20171223.0', 'user-agents==1.1.0']
DOMAIN = 'frontend' DOMAIN = 'frontend'
DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log']
@ -35,7 +35,7 @@ CONF_EXTRA_HTML_URL = 'extra_html_url'
CONF_EXTRA_HTML_URL_ES5 = 'extra_html_url_es5' CONF_EXTRA_HTML_URL_ES5 = 'extra_html_url_es5'
CONF_FRONTEND_REPO = 'development_repo' CONF_FRONTEND_REPO = 'development_repo'
CONF_JS_VERSION = 'javascript_version' CONF_JS_VERSION = 'javascript_version'
JS_DEFAULT_OPTION = 'es5' JS_DEFAULT_OPTION = 'auto'
JS_OPTIONS = ['es5', 'latest', 'auto'] JS_OPTIONS = ['es5', 'latest', 'auto']
DEFAULT_THEME_COLOR = '#03A9F4' DEFAULT_THEME_COLOR = '#03A9F4'
@ -49,7 +49,7 @@ MANIFEST_JSON = {
'lang': 'en-US', 'lang': 'en-US',
'name': 'Home Assistant', 'name': 'Home Assistant',
'short_name': 'Assistant', 'short_name': 'Assistant',
'start_url': '/', 'start_url': '/states',
'theme_color': DEFAULT_THEME_COLOR 'theme_color': DEFAULT_THEME_COLOR
} }
@ -299,8 +299,13 @@ def async_setup(hass, config):
hass.data[DATA_JS_VERSION] = js_version = conf.get(CONF_JS_VERSION) hass.data[DATA_JS_VERSION] = js_version = conf.get(CONF_JS_VERSION)
if is_dev: if is_dev:
hass.http.register_static_path( for subpath in ["src", "build-translations", "build-temp", "build",
"/home-assistant-polymer", repo_path, False) "hass_frontend", "bower_components", "panels"]:
hass.http.register_static_path(
"/home-assistant-polymer/{}".format(subpath),
os.path.join(repo_path, subpath),
False)
hass.http.register_static_path( hass.http.register_static_path(
"/static/translations", "/static/translations",
os.path.join(repo_path, "build-translations/output"), False) os.path.join(repo_path, "build-translations/output"), False)

View File

@ -12,7 +12,7 @@ from homeassistant.const import (CONF_PASSWORD, CONF_SCAN_INTERVAL,
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.discovery import load_platform from homeassistant.helpers.discovery import load_platform
REQUIREMENTS = ['pyhiveapi==0.2.5'] REQUIREMENTS = ['pyhiveapi==0.2.10']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DOMAIN = 'hive' DOMAIN = 'hive'

View File

@ -5,25 +5,26 @@ For more details about this component, please refer to the documentation at
https://home-assistant.io/components/homematic/ https://home-assistant.io/components/homematic/
""" """
import asyncio import asyncio
import os
import logging
from datetime import timedelta from datetime import timedelta
from functools import partial from functools import partial
import logging
import os
import socket
import voluptuous as vol import voluptuous as vol
import homeassistant.helpers.config_validation as cv from homeassistant.config import load_yaml_config_file
from homeassistant.const import ( from homeassistant.const import (
EVENT_HOMEASSISTANT_STOP, STATE_UNKNOWN, CONF_USERNAME, CONF_PASSWORD, EVENT_HOMEASSISTANT_STOP, CONF_USERNAME, CONF_PASSWORD, CONF_PLATFORM,
CONF_PLATFORM, CONF_HOSTS, CONF_NAME, ATTR_ENTITY_ID) CONF_HOSTS, CONF_HOST, ATTR_ENTITY_ID, STATE_UNKNOWN)
from homeassistant.helpers import discovery from homeassistant.helpers import discovery
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import track_time_interval import homeassistant.helpers.config_validation as cv
from homeassistant.config import load_yaml_config_file from homeassistant.loader import bind_hass
REQUIREMENTS = ['pyhomematic==0.1.35']
REQUIREMENTS = ['pyhomematic==0.1.36']
DOMAIN = 'homematic' DOMAIN = 'homematic'
_LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL_HUB = timedelta(seconds=300) SCAN_INTERVAL_HUB = timedelta(seconds=300)
SCAN_INTERVAL_VARIABLES = timedelta(seconds=30) SCAN_INTERVAL_VARIABLES = timedelta(seconds=30)
@ -41,9 +42,11 @@ ATTR_CHANNEL = 'channel'
ATTR_NAME = 'name' ATTR_NAME = 'name'
ATTR_ADDRESS = 'address' ATTR_ADDRESS = 'address'
ATTR_VALUE = 'value' ATTR_VALUE = 'value'
ATTR_PROXY = 'proxy' ATTR_INTERFACE = 'interface'
ATTR_ERRORCODE = 'error' ATTR_ERRORCODE = 'error'
ATTR_MESSAGE = 'message' ATTR_MESSAGE = 'message'
ATTR_MODE = 'mode'
ATTR_TIME = 'time'
EVENT_KEYPRESS = 'homematic.keypress' EVENT_KEYPRESS = 'homematic.keypress'
EVENT_IMPULSE = 'homematic.impulse' EVENT_IMPULSE = 'homematic.impulse'
@ -51,8 +54,9 @@ EVENT_ERROR = 'homematic.error'
SERVICE_VIRTUALKEY = 'virtualkey' SERVICE_VIRTUALKEY = 'virtualkey'
SERVICE_RECONNECT = 'reconnect' SERVICE_RECONNECT = 'reconnect'
SERVICE_SET_VAR_VALUE = 'set_var_value' SERVICE_SET_VARIABLE_VALUE = 'set_variable_value'
SERVICE_SET_DEV_VALUE = 'set_dev_value' SERVICE_SET_DEVICE_VALUE = 'set_device_value'
SERVICE_SET_INSTALL_MODE = 'set_install_mode'
HM_DEVICE_TYPES = { HM_DEVICE_TYPES = {
DISCOVER_SWITCHES: [ DISCOVER_SWITCHES: [
@ -73,9 +77,9 @@ HM_DEVICE_TYPES = {
'ThermostatGroup'], 'ThermostatGroup'],
DISCOVER_BINARY_SENSORS: [ DISCOVER_BINARY_SENSORS: [
'ShutterContact', 'Smoke', 'SmokeV2', 'Motion', 'MotionV2', 'ShutterContact', 'Smoke', 'SmokeV2', 'Motion', 'MotionV2',
'RemoteMotion', 'WeatherSensor', 'TiltSensor', 'IPShutterContact', 'MotionIP', 'RemoteMotion', 'WeatherSensor', 'TiltSensor',
'HMWIOSwitch', 'MaxShutterContact', 'Rain', 'WiredSensor', 'IPShutterContact', 'HMWIOSwitch', 'MaxShutterContact', 'Rain',
'PresenceIP'], 'WiredSensor', 'PresenceIP'],
DISCOVER_COVER: ['Blind', 'KeyBlind'] DISCOVER_COVER: ['Blind', 'KeyBlind']
} }
@ -90,12 +94,14 @@ HM_ATTRIBUTE_SUPPORT = {
'RSSI_DEVICE': ['rssi', {}], 'RSSI_DEVICE': ['rssi', {}],
'VALVE_STATE': ['valve', {}], 'VALVE_STATE': ['valve', {}],
'BATTERY_STATE': ['battery', {}], 'BATTERY_STATE': ['battery', {}],
'CONTROL_MODE': ['mode', {0: 'Auto', 'CONTROL_MODE': ['mode', {
1: 'Manual', 0: 'Auto',
2: 'Away', 1: 'Manual',
3: 'Boost', 2: 'Away',
4: 'Comfort', 3: 'Boost',
5: 'Lowering'}], 4: 'Comfort',
5: 'Lowering'
}],
'POWER': ['power', {}], 'POWER': ['power', {}],
'CURRENT': ['current', {}], 'CURRENT': ['current', {}],
'VOLTAGE': ['voltage', {}], 'VOLTAGE': ['voltage', {}],
@ -114,8 +120,6 @@ HM_IMPULSE_EVENTS = [
'SEQUENCE_OK', 'SEQUENCE_OK',
] ]
_LOGGER = logging.getLogger(__name__)
CONF_RESOLVENAMES_OPTIONS = [ CONF_RESOLVENAMES_OPTIONS = [
'metadata', 'metadata',
'json', 'json',
@ -124,12 +128,12 @@ CONF_RESOLVENAMES_OPTIONS = [
] ]
DATA_HOMEMATIC = 'homematic' DATA_HOMEMATIC = 'homematic'
DATA_DEVINIT = 'homematic_devinit'
DATA_STORE = 'homematic_store' DATA_STORE = 'homematic_store'
DATA_CONF = 'homematic_conf'
CONF_INTERFACES = 'interfaces'
CONF_LOCAL_IP = 'local_ip' CONF_LOCAL_IP = 'local_ip'
CONF_LOCAL_PORT = 'local_port' CONF_LOCAL_PORT = 'local_port'
CONF_IP = 'ip'
CONF_PORT = 'port' CONF_PORT = 'port'
CONF_PATH = 'path' CONF_PATH = 'path'
CONF_CALLBACK_IP = 'callback_ip' CONF_CALLBACK_IP = 'callback_ip'
@ -146,37 +150,35 @@ DEFAULT_PORT = 2001
DEFAULT_PATH = '' DEFAULT_PATH = ''
DEFAULT_USERNAME = 'Admin' DEFAULT_USERNAME = 'Admin'
DEFAULT_PASSWORD = '' DEFAULT_PASSWORD = ''
DEFAULT_VARIABLES = False
DEFAULT_DEVICES = True
DEFAULT_PRIMARY = False
DEVICE_SCHEMA = vol.Schema({ DEVICE_SCHEMA = vol.Schema({
vol.Required(CONF_PLATFORM): 'homematic', vol.Required(CONF_PLATFORM): 'homematic',
vol.Required(ATTR_NAME): cv.string, vol.Required(ATTR_NAME): cv.string,
vol.Required(ATTR_ADDRESS): cv.string, vol.Required(ATTR_ADDRESS): cv.string,
vol.Required(ATTR_PROXY): cv.string, vol.Required(ATTR_INTERFACE): cv.string,
vol.Optional(ATTR_CHANNEL, default=1): vol.Coerce(int), vol.Optional(ATTR_CHANNEL, default=1): vol.Coerce(int),
vol.Optional(ATTR_PARAM): cv.string, vol.Optional(ATTR_PARAM): cv.string,
}) })
CONFIG_SCHEMA = vol.Schema({ CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({ DOMAIN: vol.Schema({
vol.Required(CONF_HOSTS): {cv.match_all: { vol.Optional(CONF_INTERFACES, default={}): {cv.match_all: {
vol.Required(CONF_IP): cv.string, vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_PATH, default=DEFAULT_PATH): cv.string, vol.Optional(CONF_PATH, default=DEFAULT_PATH): cv.string,
vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string,
vol.Optional(CONF_PASSWORD, default=DEFAULT_PASSWORD): cv.string,
vol.Optional(CONF_VARIABLES, default=DEFAULT_VARIABLES):
cv.boolean,
vol.Optional(CONF_RESOLVENAMES, default=DEFAULT_RESOLVENAMES): vol.Optional(CONF_RESOLVENAMES, default=DEFAULT_RESOLVENAMES):
vol.In(CONF_RESOLVENAMES_OPTIONS), vol.In(CONF_RESOLVENAMES_OPTIONS),
vol.Optional(CONF_DEVICES, default=DEFAULT_DEVICES): cv.boolean, vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string,
vol.Optional(CONF_PRIMARY, default=DEFAULT_PRIMARY): cv.boolean, vol.Optional(CONF_PASSWORD, default=DEFAULT_PASSWORD): cv.string,
vol.Optional(CONF_CALLBACK_IP): cv.string, vol.Optional(CONF_CALLBACK_IP): cv.string,
vol.Optional(CONF_CALLBACK_PORT): cv.port, vol.Optional(CONF_CALLBACK_PORT): cv.port,
}}, }},
vol.Optional(CONF_HOSTS, default={}): {cv.match_all: {
vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string,
vol.Optional(CONF_PASSWORD, default=DEFAULT_PASSWORD): cv.string,
}},
vol.Optional(CONF_LOCAL_IP, default=DEFAULT_LOCAL_IP): cv.string, vol.Optional(CONF_LOCAL_IP, default=DEFAULT_LOCAL_IP): cv.string,
vol.Optional(CONF_LOCAL_PORT, default=DEFAULT_LOCAL_PORT): cv.port, vol.Optional(CONF_LOCAL_PORT, default=DEFAULT_LOCAL_PORT): cv.port,
}), }),
@ -186,61 +188,88 @@ SCHEMA_SERVICE_VIRTUALKEY = vol.Schema({
vol.Required(ATTR_ADDRESS): vol.All(cv.string, vol.Upper), vol.Required(ATTR_ADDRESS): vol.All(cv.string, vol.Upper),
vol.Required(ATTR_CHANNEL): vol.Coerce(int), vol.Required(ATTR_CHANNEL): vol.Coerce(int),
vol.Required(ATTR_PARAM): cv.string, vol.Required(ATTR_PARAM): cv.string,
vol.Optional(ATTR_PROXY): cv.string, vol.Optional(ATTR_INTERFACE): cv.string,
}) })
SCHEMA_SERVICE_SET_VAR_VALUE = vol.Schema({ SCHEMA_SERVICE_SET_VARIABLE_VALUE = vol.Schema({
vol.Required(ATTR_NAME): cv.string, vol.Required(ATTR_NAME): cv.string,
vol.Required(ATTR_VALUE): cv.match_all, vol.Required(ATTR_VALUE): cv.match_all,
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
}) })
SCHEMA_SERVICE_SET_DEV_VALUE = vol.Schema({ SCHEMA_SERVICE_SET_DEVICE_VALUE = vol.Schema({
vol.Required(ATTR_ADDRESS): vol.All(cv.string, vol.Upper), vol.Required(ATTR_ADDRESS): vol.All(cv.string, vol.Upper),
vol.Required(ATTR_CHANNEL): vol.Coerce(int), vol.Required(ATTR_CHANNEL): vol.Coerce(int),
vol.Required(ATTR_PARAM): vol.All(cv.string, vol.Upper), vol.Required(ATTR_PARAM): vol.All(cv.string, vol.Upper),
vol.Required(ATTR_VALUE): cv.match_all, vol.Required(ATTR_VALUE): cv.match_all,
vol.Optional(ATTR_PROXY): cv.string, vol.Optional(ATTR_INTERFACE): cv.string,
}) })
SCHEMA_SERVICE_RECONNECT = vol.Schema({}) SCHEMA_SERVICE_RECONNECT = vol.Schema({})
SCHEMA_SERVICE_SET_INSTALL_MODE = vol.Schema({
vol.Required(ATTR_INTERFACE): cv.string,
vol.Optional(ATTR_TIME, default=60): cv.positive_int,
vol.Optional(ATTR_MODE, default=1):
vol.All(vol.Coerce(int), vol.In([1, 2])),
vol.Optional(ATTR_ADDRESS): vol.All(cv.string, vol.Upper),
})
def virtualkey(hass, address, channel, param, proxy=None):
@bind_hass
def virtualkey(hass, address, channel, param, interface=None):
"""Send virtual keypress to homematic controlller.""" """Send virtual keypress to homematic controlller."""
data = { data = {
ATTR_ADDRESS: address, ATTR_ADDRESS: address,
ATTR_CHANNEL: channel, ATTR_CHANNEL: channel,
ATTR_PARAM: param, ATTR_PARAM: param,
ATTR_PROXY: proxy, ATTR_INTERFACE: interface,
} }
hass.services.call(DOMAIN, SERVICE_VIRTUALKEY, data) hass.services.call(DOMAIN, SERVICE_VIRTUALKEY, data)
def set_var_value(hass, entity_id, value): @bind_hass
def set_variable_value(hass, entity_id, value):
"""Change value of a Homematic system variable.""" """Change value of a Homematic system variable."""
data = { data = {
ATTR_ENTITY_ID: entity_id, ATTR_ENTITY_ID: entity_id,
ATTR_VALUE: value, ATTR_VALUE: value,
} }
hass.services.call(DOMAIN, SERVICE_SET_VAR_VALUE, data) hass.services.call(DOMAIN, SERVICE_SET_VARIABLE_VALUE, data)
def set_dev_value(hass, address, channel, param, value, proxy=None): @bind_hass
"""Call setValue XML-RPC method of supplied proxy.""" def set_device_value(hass, address, channel, param, value, interface=None):
"""Call setValue XML-RPC method of supplied interface."""
data = { data = {
ATTR_ADDRESS: address, ATTR_ADDRESS: address,
ATTR_CHANNEL: channel, ATTR_CHANNEL: channel,
ATTR_PARAM: param, ATTR_PARAM: param,
ATTR_VALUE: value, ATTR_VALUE: value,
ATTR_PROXY: proxy, ATTR_INTERFACE: interface,
} }
hass.services.call(DOMAIN, SERVICE_SET_DEV_VALUE, data) hass.services.call(DOMAIN, SERVICE_SET_DEVICE_VALUE, data)
@bind_hass
def set_install_mode(hass, interface, mode=None, time=None, address=None):
"""Call setInstallMode XML-RPC method of supplied inteface."""
data = {
key: value for key, value in (
(ATTR_INTERFACE, interface),
(ATTR_MODE, mode),
(ATTR_TIME, time),
(ATTR_ADDRESS, address)
) if value
}
hass.services.call(DOMAIN, SERVICE_SET_INSTALL_MODE, data)
@bind_hass
def reconnect(hass): def reconnect(hass):
"""Reconnect to CCU/Homegear.""" """Reconnect to CCU/Homegear."""
hass.services.call(DOMAIN, SERVICE_RECONNECT, {}) hass.services.call(DOMAIN, SERVICE_RECONNECT, {})
@ -250,31 +279,32 @@ def setup(hass, config):
"""Set up the Homematic component.""" """Set up the Homematic component."""
from pyhomematic import HMConnection from pyhomematic import HMConnection
hass.data[DATA_DEVINIT] = {} conf = config[DOMAIN]
hass.data[DATA_CONF] = remotes = {}
hass.data[DATA_STORE] = set() hass.data[DATA_STORE] = set()
# Create hosts-dictionary for pyhomematic # Create hosts-dictionary for pyhomematic
remotes = {} for rname, rconfig in conf[CONF_INTERFACES].items():
hosts = {} remotes[rname] = {
for rname, rconfig in config[DOMAIN][CONF_HOSTS].items(): 'ip': socket.gethostbyname(rconfig.get(CONF_HOST)),
server = rconfig.get(CONF_IP) 'port': rconfig.get(CONF_PORT),
'path': rconfig.get(CONF_PATH),
'resolvenames': rconfig.get(CONF_RESOLVENAMES),
'username': rconfig.get(CONF_USERNAME),
'password': rconfig.get(CONF_PASSWORD),
'callbackip': rconfig.get(CONF_CALLBACK_IP),
'callbackport': rconfig.get(CONF_CALLBACK_PORT),
'connect': True,
}
remotes[rname] = {} for sname, sconfig in conf[CONF_HOSTS].items():
remotes[rname][CONF_IP] = server remotes[sname] = {
remotes[rname][CONF_PORT] = rconfig.get(CONF_PORT) 'ip': socket.gethostbyname(sconfig.get(CONF_HOST)),
remotes[rname][CONF_PATH] = rconfig.get(CONF_PATH) 'port': DEFAULT_PORT,
remotes[rname][CONF_RESOLVENAMES] = rconfig.get(CONF_RESOLVENAMES) 'username': sconfig.get(CONF_USERNAME),
remotes[rname][CONF_USERNAME] = rconfig.get(CONF_USERNAME) 'password': sconfig.get(CONF_PASSWORD),
remotes[rname][CONF_PASSWORD] = rconfig.get(CONF_PASSWORD) 'connect': False,
remotes[rname]['callbackip'] = rconfig.get(CONF_CALLBACK_IP) }
remotes[rname]['callbackport'] = rconfig.get(CONF_CALLBACK_PORT)
if server not in hosts or rconfig.get(CONF_PRIMARY):
hosts[server] = {
CONF_VARIABLES: rconfig.get(CONF_VARIABLES),
CONF_NAME: rname,
}
hass.data[DATA_DEVINIT][rname] = rconfig.get(CONF_DEVICES)
# Create server thread # Create server thread
bound_system_callback = partial(_system_callback_handler, hass, config) bound_system_callback = partial(_system_callback_handler, hass, config)
@ -295,9 +325,8 @@ def setup(hass, config):
# Init homematic hubs # Init homematic hubs
entity_hubs = [] entity_hubs = []
for _, hub_data in hosts.items(): for hub_name in conf[CONF_HOSTS].keys():
entity_hubs.append(HMHub( entity_hubs.append(HMHub(hass, homematic, hub_name))
hass, homematic, hub_data[CONF_NAME], hub_data[CONF_VARIABLES]))
# Register HomeMatic services # Register HomeMatic services
descriptions = load_yaml_config_file( descriptions = load_yaml_config_file(
@ -331,8 +360,7 @@ def setup(hass, config):
hass.services.register( hass.services.register(
DOMAIN, SERVICE_VIRTUALKEY, _hm_service_virtualkey, DOMAIN, SERVICE_VIRTUALKEY, _hm_service_virtualkey,
descriptions[DOMAIN][SERVICE_VIRTUALKEY], descriptions[SERVICE_VIRTUALKEY], schema=SCHEMA_SERVICE_VIRTUALKEY)
schema=SCHEMA_SERVICE_VIRTUALKEY)
def _service_handle_value(service): def _service_handle_value(service):
"""Service to call setValue method for HomeMatic system variable.""" """Service to call setValue method for HomeMatic system variable."""
@ -354,9 +382,9 @@ def setup(hass, config):
hub.hm_set_variable(name, value) hub.hm_set_variable(name, value)
hass.services.register( hass.services.register(
DOMAIN, SERVICE_SET_VAR_VALUE, _service_handle_value, DOMAIN, SERVICE_SET_VARIABLE_VALUE, _service_handle_value,
descriptions[DOMAIN][SERVICE_SET_VAR_VALUE], descriptions[SERVICE_SET_VARIABLE_VALUE],
schema=SCHEMA_SERVICE_SET_VAR_VALUE) schema=SCHEMA_SERVICE_SET_VARIABLE_VALUE)
def _service_handle_reconnect(service): def _service_handle_reconnect(service):
"""Service to reconnect all HomeMatic hubs.""" """Service to reconnect all HomeMatic hubs."""
@ -364,8 +392,7 @@ def setup(hass, config):
hass.services.register( hass.services.register(
DOMAIN, SERVICE_RECONNECT, _service_handle_reconnect, DOMAIN, SERVICE_RECONNECT, _service_handle_reconnect,
descriptions[DOMAIN][SERVICE_RECONNECT], descriptions[SERVICE_RECONNECT], schema=SCHEMA_SERVICE_RECONNECT)
schema=SCHEMA_SERVICE_RECONNECT)
def _service_handle_device(service): def _service_handle_device(service):
"""Service to call setValue method for HomeMatic devices.""" """Service to call setValue method for HomeMatic devices."""
@ -383,9 +410,23 @@ def setup(hass, config):
hmdevice.setValue(param, value, channel) hmdevice.setValue(param, value, channel)
hass.services.register( hass.services.register(
DOMAIN, SERVICE_SET_DEV_VALUE, _service_handle_device, DOMAIN, SERVICE_SET_DEVICE_VALUE, _service_handle_device,
descriptions[DOMAIN][SERVICE_SET_DEV_VALUE], descriptions[SERVICE_SET_DEVICE_VALUE],
schema=SCHEMA_SERVICE_SET_DEV_VALUE) schema=SCHEMA_SERVICE_SET_DEVICE_VALUE)
def _service_handle_install_mode(service):
"""Service to set interface into install mode."""
interface = service.data.get(ATTR_INTERFACE)
mode = service.data.get(ATTR_MODE)
time = service.data.get(ATTR_TIME)
address = service.data.get(ATTR_ADDRESS)
homematic.setInstallMode(interface, t=time, mode=mode, address=address)
hass.services.register(
DOMAIN, SERVICE_SET_INSTALL_MODE, _service_handle_install_mode,
descriptions[SERVICE_SET_INSTALL_MODE],
schema=SCHEMA_SERVICE_SET_INSTALL_MODE)
return True return True
@ -395,10 +436,10 @@ def _system_callback_handler(hass, config, src, *args):
# New devices available at hub # New devices available at hub
if src == 'newDevices': if src == 'newDevices':
(interface_id, dev_descriptions) = args (interface_id, dev_descriptions) = args
proxy = interface_id.split('-')[-1] interface = interface_id.split('-')[-1]
# Device support active? # Device support active?
if not hass.data[DATA_DEVINIT][proxy]: if not hass.data[DATA_CONF][interface]['connect']:
return return
addresses = [] addresses = []
@ -410,9 +451,9 @@ def _system_callback_handler(hass, config, src, *args):
# Register EVENTS # Register EVENTS
# Search all devices with an EVENTNODE that includes data # Search all devices with an EVENTNODE that includes data
bound_event_callback = partial(_hm_event_handler, hass, proxy) bound_event_callback = partial(_hm_event_handler, hass, interface)
for dev in addresses: for dev in addresses:
hmdevice = hass.data[DATA_HOMEMATIC].devices[proxy].get(dev) hmdevice = hass.data[DATA_HOMEMATIC].devices[interface].get(dev)
if hmdevice.EVENTNODE: if hmdevice.EVENTNODE:
hmdevice.setEventCallback( hmdevice.setEventCallback(
@ -429,7 +470,7 @@ def _system_callback_handler(hass, config, src, *args):
('climate', DISCOVER_CLIMATE)): ('climate', DISCOVER_CLIMATE)):
# Get all devices of a specific type # Get all devices of a specific type
found_devices = _get_devices( found_devices = _get_devices(
hass, discovery_type, addresses, proxy) hass, discovery_type, addresses, interface)
# When devices of this type are found # When devices of this type are found
# they are setup in HASS and an discovery event is fired # they are setup in HASS and an discovery event is fired
@ -448,12 +489,12 @@ def _system_callback_handler(hass, config, src, *args):
}) })
def _get_devices(hass, discovery_type, keys, proxy): def _get_devices(hass, discovery_type, keys, interface):
"""Get the HomeMatic devices for given discovery_type.""" """Get the HomeMatic devices for given discovery_type."""
device_arr = [] device_arr = []
for key in keys: for key in keys:
device = hass.data[DATA_HOMEMATIC].devices[proxy][key] device = hass.data[DATA_HOMEMATIC].devices[interface][key]
class_name = device.__class__.__name__ class_name = device.__class__.__name__
metadata = {} metadata = {}
@ -485,7 +526,7 @@ def _get_devices(hass, discovery_type, keys, proxy):
device_dict = { device_dict = {
CONF_PLATFORM: "homematic", CONF_PLATFORM: "homematic",
ATTR_ADDRESS: key, ATTR_ADDRESS: key,
ATTR_PROXY: proxy, ATTR_INTERFACE: interface,
ATTR_NAME: name, ATTR_NAME: name,
ATTR_CHANNEL: channel ATTR_CHANNEL: channel
} }
@ -521,12 +562,12 @@ def _create_ha_name(name, channel, param, count):
return "{} {} {}".format(name, channel, param) return "{} {} {}".format(name, channel, param)
def _hm_event_handler(hass, proxy, device, caller, attribute, value): def _hm_event_handler(hass, interface, device, caller, attribute, value):
"""Handle all pyhomematic device events.""" """Handle all pyhomematic device events."""
try: try:
channel = int(device.split(":")[1]) channel = int(device.split(":")[1])
address = device.split(":")[0] address = device.split(":")[0]
hmdevice = hass.data[DATA_HOMEMATIC].devices[proxy].get(address) hmdevice = hass.data[DATA_HOMEMATIC].devices[interface].get(address)
except (TypeError, ValueError): except (TypeError, ValueError):
_LOGGER.error("Event handling channel convert error!") _LOGGER.error("Event handling channel convert error!")
return return
@ -561,14 +602,14 @@ def _hm_event_handler(hass, proxy, device, caller, attribute, value):
def _device_from_servicecall(hass, service): def _device_from_servicecall(hass, service):
"""Extract HomeMatic device from service call.""" """Extract HomeMatic device from service call."""
address = service.data.get(ATTR_ADDRESS) address = service.data.get(ATTR_ADDRESS)
proxy = service.data.get(ATTR_PROXY) interface = service.data.get(ATTR_INTERFACE)
if address == 'BIDCOS-RF': if address == 'BIDCOS-RF':
address = 'BidCoS-RF' address = 'BidCoS-RF'
if proxy: if interface:
return hass.data[DATA_HOMEMATIC].devices[proxy].get(address) return hass.data[DATA_HOMEMATIC].devices[interface].get(address)
for _, devices in hass.data[DATA_HOMEMATIC].devices.items(): for devices in hass.data[DATA_HOMEMATIC].devices.values():
if address in devices: if address in devices:
return devices[address] return devices[address]
@ -576,25 +617,23 @@ def _device_from_servicecall(hass, service):
class HMHub(Entity): class HMHub(Entity):
"""The HomeMatic hub. (CCU2/HomeGear).""" """The HomeMatic hub. (CCU2/HomeGear)."""
def __init__(self, hass, homematic, name, use_variables): def __init__(self, hass, homematic, name):
"""Initialize HomeMatic hub.""" """Initialize HomeMatic hub."""
self.hass = hass self.hass = hass
self.entity_id = "{}.{}".format(DOMAIN, name.lower()) self.entity_id = "{}.{}".format(DOMAIN, name.lower())
self._homematic = homematic self._homematic = homematic
self._variables = {} self._variables = {}
self._name = name self._name = name
self._state = STATE_UNKNOWN self._state = None
self._use_variables = use_variables
# Load data # Load data
track_time_interval( self.hass.helpers.event.track_time_interval(
self.hass, self._update_hub, SCAN_INTERVAL_HUB) self._update_hub, SCAN_INTERVAL_HUB)
self.hass.add_job(self._update_hub, None) self.hass.add_job(self._update_hub, None)
if self._use_variables: self.hass.helpers.event.track_time_interval(
track_time_interval( self._update_variables, SCAN_INTERVAL_VARIABLES)
self.hass, self._update_variables, SCAN_INTERVAL_VARIABLES) self.hass.add_job(self._update_variables, None)
self.hass.add_job(self._update_variables, None)
@property @property
def name(self): def name(self):
@ -672,7 +711,7 @@ class HMDevice(Entity):
"""Initialize a generic HomeMatic device.""" """Initialize a generic HomeMatic device."""
self._name = config.get(ATTR_NAME) self._name = config.get(ATTR_NAME)
self._address = config.get(ATTR_ADDRESS) self._address = config.get(ATTR_ADDRESS)
self._proxy = config.get(ATTR_PROXY) self._interface = config.get(ATTR_INTERFACE)
self._channel = config.get(ATTR_CHANNEL) self._channel = config.get(ATTR_CHANNEL)
self._state = config.get(ATTR_PARAM) self._state = config.get(ATTR_PARAM)
self._data = {} self._data = {}
@ -700,11 +739,6 @@ class HMDevice(Entity):
"""Return the name of the device.""" """Return the name of the device."""
return self._name return self._name
@property
def assumed_state(self):
"""Return true if unable to access real state of the device."""
return not self._available
@property @property
def available(self): def available(self):
"""Return true if device is available.""" """Return true if device is available."""
@ -715,10 +749,6 @@ class HMDevice(Entity):
"""Return device specific state attributes.""" """Return device specific state attributes."""
attr = {} attr = {}
# No data available
if not self.available:
return attr
# Generate a dictionary with attributes # Generate a dictionary with attributes
for node, data in HM_ATTRIBUTE_SUPPORT.items(): for node, data in HM_ATTRIBUTE_SUPPORT.items():
# Is an attribute and exists for this object # Is an attribute and exists for this object
@ -728,7 +758,7 @@ class HMDevice(Entity):
# Static attributes # Static attributes
attr['id'] = self._hmdevice.ADDRESS attr['id'] = self._hmdevice.ADDRESS
attr['proxy'] = self._proxy attr['interface'] = self._interface
return attr return attr
@ -739,7 +769,8 @@ class HMDevice(Entity):
# Initialize # Initialize
self._homematic = self.hass.data[DATA_HOMEMATIC] self._homematic = self.hass.data[DATA_HOMEMATIC]
self._hmdevice = self._homematic.devices[self._proxy][self._address] self._hmdevice = \
self._homematic.devices[self._interface][self._address]
self._connected = True self._connected = True
try: try:
@ -773,6 +804,9 @@ class HMDevice(Entity):
if attribute == 'UNREACH': if attribute == 'UNREACH':
self._available = bool(value) self._available = bool(value)
has_changed = True has_changed = True
elif not self.available:
self._available = False
has_changed = True
# If it has changed data point, update HASS # If it has changed data point, update HASS
if has_changed: if has_changed:

View File

@ -0,0 +1,68 @@
# Describes the format for available component services
virtualkey:
description: Press a virtual key from CCU/Homegear or simulate keypress.
fields:
address:
description: Address of homematic device or BidCoS-RF for virtual remote.
example: BidCoS-RF
channel:
description: Channel for calling a keypress.
example: 1
param:
description: Event to send i.e. PRESS_LONG, PRESS_SHORT.
example: PRESS_LONG
interface:
description: (Optional) for set a interface value.
example: Interfaces name from config
set_variable_value:
description: Set the name of a node.
fields:
entity_id:
description: Name(s) of homematic central to set value.
example: 'homematic.ccu2'
name:
description: Name of the variable to set.
example: 'testvariable'
value:
description: New value
example: 1
set_device_value:
description: Set a device property on RPC XML interface.
fields:
address:
description: Address of homematic device or BidCoS-RF for virtual remote
example: BidCoS-RF
channel:
description: Channel for calling a keypress
example: 1
param:
description: Event to send i.e. PRESS_LONG, PRESS_SHORT
example: PRESS_LONG
interface:
description: (Optional) for set a interface value
example: Interfaces name from config
value:
description: New value
example: 1
reconnect:
description: Reconnect to all Homematic Hubs.
set_install_mode:
description: Set a RPC XML interface into installation mode.
fields:
interface:
description: Select the given interface into install mode
example: Interfaces name from config
mode:
description: (Default 1) 1= Normal mode / 2= Remove exists old links
example: 1
time:
description: (Default 60) Time in seconds to run in install mode
example: 1
address:
description: (Optional) Address of homematic device or BidCoS-RF to learn
example: LEQ3948571

View File

@ -0,0 +1,246 @@
"""
This component provides basic support for the Philips Hue system.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/hue/
"""
import json
import logging
import os
import socket
import voluptuous as vol
from homeassistant.components.discovery import SERVICE_HUE
from homeassistant.config import load_yaml_config_file
from homeassistant.const import CONF_FILENAME, CONF_HOST
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers import discovery
REQUIREMENTS = ['phue==1.0']
_LOGGER = logging.getLogger(__name__)
DOMAIN = "hue"
SERVICE_HUE_SCENE = "hue_activate_scene"
CONF_BRIDGES = "bridges"
CONF_ALLOW_UNREACHABLE = 'allow_unreachable'
DEFAULT_ALLOW_UNREACHABLE = False
PHUE_CONFIG_FILE = 'phue.conf'
CONF_ALLOW_IN_EMULATED_HUE = "allow_in_emulated_hue"
DEFAULT_ALLOW_IN_EMULATED_HUE = True
CONF_ALLOW_HUE_GROUPS = "allow_hue_groups"
DEFAULT_ALLOW_HUE_GROUPS = True
BRIDGE_CONFIG_SCHEMA = vol.Schema([{
vol.Optional(CONF_HOST): cv.string,
vol.Optional(CONF_FILENAME, default=PHUE_CONFIG_FILE): cv.string,
vol.Optional(CONF_ALLOW_UNREACHABLE,
default=DEFAULT_ALLOW_UNREACHABLE): cv.boolean,
vol.Optional(CONF_ALLOW_IN_EMULATED_HUE,
default=DEFAULT_ALLOW_IN_EMULATED_HUE): cv.boolean,
vol.Optional(CONF_ALLOW_HUE_GROUPS,
default=DEFAULT_ALLOW_HUE_GROUPS): cv.boolean,
}])
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Optional(CONF_BRIDGES, default=[]): BRIDGE_CONFIG_SCHEMA,
}),
}, extra=vol.ALLOW_EXTRA)
ATTR_GROUP_NAME = "group_name"
ATTR_SCENE_NAME = "scene_name"
SCENE_SCHEMA = vol.Schema({
vol.Required(ATTR_GROUP_NAME): cv.string,
vol.Required(ATTR_SCENE_NAME): cv.string,
})
CONFIG_INSTRUCTIONS = """
Press the button on the bridge to register Philips Hue with Home Assistant.
![Location of button on bridge](/static/images/config_philips_hue.jpg)
"""
def setup(hass, config):
"""Set up the Hue platform."""
config = config.get(DOMAIN)
if config is None:
config = {}
if DOMAIN not in hass.data:
hass.data[DOMAIN] = {}
discovery.listen(
hass,
SERVICE_HUE,
lambda service, discovery_info:
bridge_discovered(hass, service, discovery_info))
bridges = config.get(CONF_BRIDGES, [])
for bridge in bridges:
filename = bridge.get(CONF_FILENAME)
allow_unreachable = bridge.get(CONF_ALLOW_UNREACHABLE)
allow_in_emulated_hue = bridge.get(CONF_ALLOW_IN_EMULATED_HUE)
allow_hue_groups = bridge.get(CONF_ALLOW_HUE_GROUPS)
host = bridge.get(CONF_HOST)
if host is None:
host = _find_host_from_config(hass, filename)
if host is None:
_LOGGER.error("No host found in configuration")
return False
setup_bridge(host, hass, filename, allow_unreachable,
allow_in_emulated_hue, allow_hue_groups)
return True
def bridge_discovered(hass, service, discovery_info):
"""Dispatcher for Hue discovery events."""
if "HASS Bridge" in discovery_info.get('name', ''):
return
host = discovery_info.get('host')
serial = discovery_info.get('serial')
filename = 'phue-{}.conf'.format(serial)
setup_bridge(host, hass, filename)
def setup_bridge(host, hass, filename=None, allow_unreachable=False,
allow_in_emulated_hue=True, allow_hue_groups=True):
"""Set up a given Hue bridge."""
# Only register a device once
if socket.gethostbyname(host) in hass.data[DOMAIN]:
return
bridge = HueBridge(host, hass, filename, allow_unreachable,
allow_in_emulated_hue, allow_hue_groups)
bridge.setup()
def _find_host_from_config(hass, filename=PHUE_CONFIG_FILE):
"""Attempt to detect host based on existing configuration."""
path = hass.config.path(filename)
if not os.path.isfile(path):
return None
try:
with open(path) as inp:
return next(iter(json.load(inp).keys()))
except (ValueError, AttributeError, StopIteration):
# ValueError if can't parse as JSON
# AttributeError if JSON value is not a dict
# StopIteration if no keys
return None
class HueBridge(object):
"""Manages a single Hue bridge."""
def __init__(self, host, hass, filename, allow_unreachable=False,
allow_in_emulated_hue=True, allow_hue_groups=True):
"""Initialize the system."""
self.host = host
self.hass = hass
self.filename = filename
self.allow_unreachable = allow_unreachable
self.allow_in_emulated_hue = allow_in_emulated_hue
self.allow_hue_groups = allow_hue_groups
self.bridge = None
self.lights = {}
self.lightgroups = {}
self.configured = False
self.config_request_id = None
hass.data[DOMAIN][socket.gethostbyname(host)] = self
def setup(self):
"""Set up a phue bridge based on host parameter."""
import phue
try:
self.bridge = phue.Bridge(
self.host,
config_file_path=self.hass.config.path(self.filename))
except ConnectionRefusedError: # Wrong host was given
_LOGGER.error("Error connecting to the Hue bridge at %s",
self.host)
return
except phue.PhueRegistrationException:
_LOGGER.warning("Connected to Hue at %s but not registered.",
self.host)
self.request_configuration()
return
# If we came here and configuring this host, mark as done
if self.config_request_id:
request_id = self.config_request_id
self.config_request_id = None
configurator = self.hass.components.configurator
configurator.request_done(request_id)
self.configured = True
discovery.load_platform(
self.hass, 'light', DOMAIN,
{'bridge_id': socket.gethostbyname(self.host)})
# create a service for calling run_scene directly on the bridge,
# used to simplify automation rules.
def hue_activate_scene(call):
"""Service to call directly into bridge to set scenes."""
group_name = call.data[ATTR_GROUP_NAME]
scene_name = call.data[ATTR_SCENE_NAME]
self.bridge.run_scene(group_name, scene_name)
descriptions = load_yaml_config_file(
os.path.join(os.path.dirname(__file__), 'services.yaml'))
self.hass.services.register(
DOMAIN, SERVICE_HUE_SCENE, hue_activate_scene,
descriptions.get(SERVICE_HUE_SCENE),
schema=SCENE_SCHEMA)
def request_configuration(self):
"""Request configuration steps from the user."""
configurator = self.hass.components.configurator
# We got an error if this method is called while we are configuring
if self.config_request_id:
configurator.notify_errors(
self.config_request_id,
"Failed to register, please try again.")
return
self.config_request_id = configurator.request_config(
"Philips Hue",
lambda data: self.setup(),
description=CONFIG_INSTRUCTIONS,
entity_picture="/static/images/logo_philips_hue.png",
submit_caption="I have pressed the button"
)
def get_api(self):
"""Return the full api dictionary from phue."""
return self.bridge.get_api()
def set_light(self, light_id, command):
"""Adjust properties of one or more lights. See phue for details."""
return self.bridge.set_light(light_id, command)
def set_group(self, light_id, command):
"""Change light settings for a group. See phue for detail."""
return self.bridge.set_group(light_id, command)

View File

@ -4,6 +4,7 @@ Support the ISY-994 controllers.
For configuration details please visit the documentation for this component at For configuration details please visit the documentation for this component at
https://home-assistant.io/components/isy994/ https://home-assistant.io/components/isy994/
""" """
import asyncio
from collections import namedtuple from collections import namedtuple
import logging import logging
from urllib.parse import urlparse from urllib.parse import urlparse
@ -17,7 +18,7 @@ from homeassistant.helpers import discovery, config_validation as cv
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.helpers.typing import ConfigType, Dict # noqa from homeassistant.helpers.typing import ConfigType, Dict # noqa
REQUIREMENTS = ['PyISY==1.0.8'] REQUIREMENTS = ['PyISY==1.1.0']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -91,6 +92,34 @@ def filter_nodes(nodes: list, units: list=None, states: list=None) -> list:
return filtered_nodes return filtered_nodes
def _is_node_a_sensor(node, path: str, sensor_identifier: str) -> bool:
"""Determine if the given node is a sensor."""
if not isinstance(node, PYISY.Nodes.Node):
return False
if sensor_identifier in path or sensor_identifier in node.name:
return True
# This method is most reliable but only works on 5.x firmware
try:
if node.node_def_id == 'BinaryAlarm':
return True
except AttributeError:
pass
# This method works on all firmwares, but only for Insteon devices
try:
device_type = node.type
except AttributeError:
# Node has no type; most likely not an Insteon device
pass
else:
split_type = device_type.split('.')
return split_type[0] == '16' # 16 represents Insteon binary sensors
return False
def _categorize_nodes(hidden_identifier: str, sensor_identifier: str) -> None: def _categorize_nodes(hidden_identifier: str, sensor_identifier: str) -> None:
"""Categorize the ISY994 nodes.""" """Categorize the ISY994 nodes."""
global SENSOR_NODES global SENSOR_NODES
@ -106,7 +135,7 @@ def _categorize_nodes(hidden_identifier: str, sensor_identifier: str) -> None:
hidden = hidden_identifier in path or hidden_identifier in node.name hidden = hidden_identifier in path or hidden_identifier in node.name
if hidden: if hidden:
node.name += hidden_identifier node.name += hidden_identifier
if sensor_identifier in path or sensor_identifier in node.name: if _is_node_a_sensor(node, path, sensor_identifier):
SENSOR_NODES.append(node) SENSOR_NODES.append(node)
elif isinstance(node, PYISY.Nodes.Node): elif isinstance(node, PYISY.Nodes.Node):
NODES.append(node) NODES.append(node)
@ -227,15 +256,31 @@ class ISYDevice(Entity):
def __init__(self, node) -> None: def __init__(self, node) -> None:
"""Initialize the insteon device.""" """Initialize the insteon device."""
self._node = node self._node = node
self._change_handler = None
self._control_handler = None
@asyncio.coroutine
def async_added_to_hass(self) -> None:
"""Subscribe to the node change events."""
self._change_handler = self._node.status.subscribe( self._change_handler = self._node.status.subscribe(
'changed', self.on_update) 'changed', self.on_update)
if hasattr(self._node, 'controlEvents'):
self._control_handler = self._node.controlEvents.subscribe(
self.on_control)
# pylint: disable=unused-argument # pylint: disable=unused-argument
def on_update(self, event: object) -> None: def on_update(self, event: object) -> None:
"""Handle the update event from the ISY994 Node.""" """Handle the update event from the ISY994 Node."""
self.schedule_update_ha_state() self.schedule_update_ha_state()
def on_control(self, event: object) -> None:
"""Handle a control event from the ISY994 Node."""
self.hass.bus.fire('isy994_control', {
'entity_id': self.entity_id,
'control': event
})
@property @property
def domain(self) -> str: def domain(self) -> str:
"""Get the domain of the device.""" """Get the domain of the device."""
@ -270,6 +315,21 @@ class ISYDevice(Entity):
# pylint: disable=protected-access # pylint: disable=protected-access
return self._node.status._val return self._node.status._val
def is_unknown(self) -> bool:
"""Get whether or not the value of this Entity's node is unknown.
PyISY reports unknown values as -inf
"""
return self.value == -1 * float('inf')
@property
def state(self):
"""Return the state of the ISY device."""
if self.is_unknown():
return None
else:
return super().state
@property @property
def device_state_attributes(self) -> Dict: def device_state_attributes(self) -> Dict:
"""Get the state attributes for the device.""" """Get the state attributes for the device."""

View File

@ -21,7 +21,7 @@ REQUIREMENTS = ['evdev==0.6.1']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DEVICE_DESCRIPTOR = 'device_descriptor' DEVICE_DESCRIPTOR = 'device_descriptor'
DEVICE_ID_GROUP = 'Device descriptor or name' DEVICE_ID_GROUP = 'Device description'
DEVICE_NAME = 'device_name' DEVICE_NAME = 'device_name'
DOMAIN = 'keyboard_remote' DOMAIN = 'keyboard_remote'
@ -36,12 +36,13 @@ KEYBOARD_REMOTE_DISCONNECTED = 'keyboard_remote_disconnected'
TYPE = 'type' TYPE = 'type'
CONFIG_SCHEMA = vol.Schema({ CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({ DOMAIN:
vol.Exclusive(DEVICE_DESCRIPTOR, DEVICE_ID_GROUP): cv.string, vol.All(cv.ensure_list, [vol.Schema({
vol.Exclusive(DEVICE_NAME, DEVICE_ID_GROUP): cv.string, vol.Exclusive(DEVICE_DESCRIPTOR, DEVICE_ID_GROUP): cv.string,
vol.Optional(TYPE, default='key_up'): vol.Exclusive(DEVICE_NAME, DEVICE_ID_GROUP): cv.string,
vol.All(cv.string, vol.Any('key_up', 'key_down', 'key_hold')), vol.Optional(TYPE, default='key_up'):
}), vol.All(cv.string, vol.Any('key_up', 'key_down', 'key_hold'))
})])
}, extra=vol.ALLOW_EXTRA) }, extra=vol.ALLOW_EXTRA)
@ -49,11 +50,6 @@ def setup(hass, config):
"""Set up the keyboard_remote.""" """Set up the keyboard_remote."""
config = config.get(DOMAIN) config = config.get(DOMAIN)
if not config.get(DEVICE_DESCRIPTOR) and\
not config.get(DEVICE_NAME):
_LOGGER.error("No device_descriptor or device_name found")
return
keyboard_remote = KeyboardRemote( keyboard_remote = KeyboardRemote(
hass, hass,
config config
@ -63,7 +59,7 @@ def setup(hass, config):
keyboard_remote.run() keyboard_remote.run()
def _stop_keyboard_remote(_event): def _stop_keyboard_remote(_event):
keyboard_remote.stopped.set() keyboard_remote.stop()
hass.bus.listen_once( hass.bus.listen_once(
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_START,
@ -77,19 +73,21 @@ def setup(hass, config):
return True return True
class KeyboardRemote(threading.Thread): class KeyboardRemoteThread(threading.Thread):
"""This interfaces with the inputdevice using evdev.""" """This interfaces with the inputdevice using evdev."""
def __init__(self, hass, config): def __init__(self, hass, device_name, device_descriptor, key_value):
"""Construct a KeyboardRemote interface object.""" """Construct a thread listening for events on one device."""
from evdev import InputDevice, list_devices self.hass = hass
self.device_name = device_name
self.device_descriptor = device_descriptor
self.key_value = key_value
self.device_descriptor = config.get(DEVICE_DESCRIPTOR)
self.device_name = config.get(DEVICE_NAME)
if self.device_descriptor: if self.device_descriptor:
self.device_id = self.device_descriptor self.device_id = self.device_descriptor
else: else:
self.device_id = self.device_name self.device_id = self.device_name
self.dev = self._get_keyboard_device() self.dev = self._get_keyboard_device()
if self.dev is not None: if self.dev is not None:
_LOGGER.debug("Keyboard connected, %s", self.device_id) _LOGGER.debug("Keyboard connected, %s", self.device_id)
@ -103,6 +101,7 @@ class KeyboardRemote(threading.Thread):
id_folder = '/dev/input/by-id/' id_folder = '/dev/input/by-id/'
if os.path.isdir(id_folder): if os.path.isdir(id_folder):
from evdev import InputDevice, list_devices
device_names = [InputDevice(file_name).name device_names = [InputDevice(file_name).name
for file_name in list_devices()] for file_name in list_devices()]
_LOGGER.debug( _LOGGER.debug(
@ -116,7 +115,6 @@ class KeyboardRemote(threading.Thread):
threading.Thread.__init__(self) threading.Thread.__init__(self)
self.stopped = threading.Event() self.stopped = threading.Event()
self.hass = hass self.hass = hass
self.key_value = KEY_VALUE.get(config.get(TYPE, 'key_up'))
def _get_keyboard_device(self): def _get_keyboard_device(self):
"""Get the keyboard device.""" """Get the keyboard device."""
@ -145,7 +143,7 @@ class KeyboardRemote(threading.Thread):
while not self.stopped.isSet(): while not self.stopped.isSet():
# Sleeps to ease load on processor # Sleeps to ease load on processor
time.sleep(.1) time.sleep(.05)
if self.dev is None: if self.dev is None:
self.dev = self._get_keyboard_device() self.dev = self._get_keyboard_device()
@ -178,3 +176,32 @@ class KeyboardRemote(threading.Thread):
KEYBOARD_REMOTE_COMMAND_RECEIVED, KEYBOARD_REMOTE_COMMAND_RECEIVED,
{KEY_CODE: event.code} {KEY_CODE: event.code}
) )
class KeyboardRemote(object):
"""Sets up one thread per device."""
def __init__(self, hass, config):
"""Construct a KeyboardRemote interface object."""
self.threads = []
for dev_block in config:
device_descriptor = dev_block.get(DEVICE_DESCRIPTOR)
device_name = dev_block.get(DEVICE_NAME)
key_value = KEY_VALUE.get(dev_block.get(TYPE, 'key_up'))
if device_descriptor is not None\
or device_name is not None:
thread = KeyboardRemoteThread(hass, device_name,
device_descriptor,
key_value)
self.threads.append(thread)
def run(self):
"""Run all event listener threads."""
for thread in self.threads:
thread.start()
def stop(self):
"""Stop all event listener threads."""
for thread in self.threads:
thread.stopped.set()

View File

@ -0,0 +1,112 @@
"""
Support for Greenwave Reality (TCP Connected) lights.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/light.greenwave/
"""
import logging
import voluptuous as vol
from homeassistant.components.light import (
ATTR_BRIGHTNESS, Light, PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS)
from homeassistant.const import CONF_HOST
import homeassistant.helpers.config_validation as cv
SUPPORTED_FEATURES = (SUPPORT_BRIGHTNESS)
REQUIREMENTS = ['greenwavereality==0.2.9']
_LOGGER = logging.getLogger(__name__)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,
vol.Required("version"): cv.positive_int,
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup Greenwave Reality Platform."""
import greenwavereality as greenwave
import os
host = config.get(CONF_HOST)
tokenfile = hass.config.path('.greenwave')
if config.get("version") == 3:
if os.path.exists(tokenfile):
tokenfile = open(tokenfile)
token = tokenfile.read()
tokenfile.close()
else:
token = greenwave.grab_token(host, 'hass', 'homeassistant')
tokenfile = open(tokenfile, "w+")
tokenfile.write(token)
tokenfile.close()
else:
token = None
doc = greenwave.grab_xml(host, token)
add_devices(GreenwaveLight(device, host, token) for device in doc)
class GreenwaveLight(Light):
"""Representation of an Greenwave Reality Light."""
def __init__(self, light, host, token):
"""Initialize a Greenwave Reality Light."""
import greenwavereality as greenwave
self._did = light['did']
self._name = light['name']
self._state = int(light['state'])
self._brightness = greenwave.hass_brightness(light)
self._host = host
self._online = greenwave.check_online(light)
self.token = token
@property
def supported_features(self):
"""Flag supported features."""
return SUPPORTED_FEATURES
@property
def available(self):
"""Return True if entity is available."""
return self._online
@property
def name(self):
"""Return the display name of this light."""
return self._name
@property
def brightness(self):
"""Return the brightness of the light."""
return self._brightness
@property
def is_on(self):
"""Return true if light is on."""
return self._state
def turn_on(self, **kwargs):
"""Instruct the light to turn on."""
import greenwavereality as greenwave
temp_brightness = int((kwargs.get(ATTR_BRIGHTNESS, 255)
/ 255) * 100)
greenwave.set_brightness(self._host, self._did,
temp_brightness, self.token)
greenwave.turn_on(self._host, self._did, self.token)
def turn_off(self, **kwargs):
"""Instruct the light to turn off."""
import greenwavereality as greenwave
greenwave.turn_off(self._host, self._did, self.token)
def update(self):
"""Fetch new state data for this light."""
import greenwavereality as greenwave
doc = greenwave.grab_xml(self._host, self.token)
for device in doc:
if device['did'] == self._did:
self._state = int(device['state'])
self._brightness = greenwave.hass_brightness(device)
self._online = greenwave.check_online(device)
self._name = device['name']

View File

@ -4,8 +4,10 @@ Support for the Hive devices.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/light.hive/ https://home-assistant.io/components/light.hive/
""" """
import colorsys
from homeassistant.components.hive import DATA_HIVE from homeassistant.components.hive import DATA_HIVE
from homeassistant.components.light import (ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, from homeassistant.components.light import (ATTR_BRIGHTNESS, ATTR_COLOR_TEMP,
ATTR_RGB_COLOR,
SUPPORT_BRIGHTNESS, SUPPORT_BRIGHTNESS,
SUPPORT_COLOR_TEMP, SUPPORT_COLOR_TEMP,
SUPPORT_RGB_COLOR, Light) SUPPORT_RGB_COLOR, Light)
@ -46,19 +48,24 @@ class HiveDeviceLight(Light):
"""Return the display name of this light.""" """Return the display name of this light."""
return self.node_name return self.node_name
@property
def brightness(self):
"""Brightness of the light (an integer in the range 1-255)."""
return self.session.light.get_brightness(self.node_id)
@property @property
def min_mireds(self): def min_mireds(self):
"""Return the coldest color_temp that this light supports.""" """Return the coldest color_temp that this light supports."""
if self.light_device_type == "tuneablelight" \ if self.light_device_type == "tuneablelight" \
or self.light_device_type == "colourtuneablelight": or self.light_device_type == "colourtuneablelight":
return self.session.light.get_min_colour_temp(self.node_id) return self.session.light.get_min_color_temp(self.node_id)
@property @property
def max_mireds(self): def max_mireds(self):
"""Return the warmest color_temp that this light supports.""" """Return the warmest color_temp that this light supports."""
if self.light_device_type == "tuneablelight" \ if self.light_device_type == "tuneablelight" \
or self.light_device_type == "colourtuneablelight": or self.light_device_type == "colourtuneablelight":
return self.session.light.get_max_colour_temp(self.node_id) return self.session.light.get_max_color_temp(self.node_id)
@property @property
def color_temp(self): def color_temp(self):
@ -68,9 +75,10 @@ class HiveDeviceLight(Light):
return self.session.light.get_color_temp(self.node_id) return self.session.light.get_color_temp(self.node_id)
@property @property
def brightness(self): def rgb_color(self) -> tuple:
"""Brightness of the light (an integer in the range 1-255).""" """Return the RBG color value."""
return self.session.light.get_brightness(self.node_id) if self.light_device_type == "colourtuneablelight":
return self.session.light.get_color(self.node_id)
@property @property
def is_on(self): def is_on(self):
@ -81,6 +89,7 @@ class HiveDeviceLight(Light):
"""Instruct the light to turn on.""" """Instruct the light to turn on."""
new_brightness = None new_brightness = None
new_color_temp = None new_color_temp = None
new_color = None
if ATTR_BRIGHTNESS in kwargs: if ATTR_BRIGHTNESS in kwargs:
tmp_new_brightness = kwargs.get(ATTR_BRIGHTNESS) tmp_new_brightness = kwargs.get(ATTR_BRIGHTNESS)
percentage_brightness = ((tmp_new_brightness / 255) * 100) percentage_brightness = ((tmp_new_brightness / 255) * 100)
@ -90,13 +99,19 @@ class HiveDeviceLight(Light):
if ATTR_COLOR_TEMP in kwargs: if ATTR_COLOR_TEMP in kwargs:
tmp_new_color_temp = kwargs.get(ATTR_COLOR_TEMP) tmp_new_color_temp = kwargs.get(ATTR_COLOR_TEMP)
new_color_temp = round(1000000 / tmp_new_color_temp) new_color_temp = round(1000000 / tmp_new_color_temp)
if ATTR_RGB_COLOR in kwargs:
get_new_color = kwargs.get(ATTR_RGB_COLOR)
tmp_new_color = colorsys.rgb_to_hsv(get_new_color[0],
get_new_color[1],
get_new_color[2])
hue = int(round(tmp_new_color[0] * 360))
saturation = int(round(tmp_new_color[1] * 100))
value = int(round((tmp_new_color[2] / 255) * 100))
new_color = (hue, saturation, value)
if new_brightness is not None: self.session.light.turn_on(self.node_id, self.light_device_type,
self.session.light.set_brightness(self.node_id, new_brightness) new_brightness, new_color_temp,
elif new_color_temp is not None: new_color)
self.session.light.set_colour_temp(self.node_id, new_color_temp)
else:
self.session.light.turn_on(self.node_id)
for entity in self.session.entities: for entity in self.session.entities:
entity.handle_update(self.data_updatesource) entity.handle_update(self.data_updatesource)

View File

@ -1,19 +1,21 @@
""" """
Support for Hue lights. This component provides light support for the Philips Hue system.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/light.hue/ https://home-assistant.io/components/light.hue/
""" """
import json
import logging
import os
import random
import socket
from datetime import timedelta from datetime import timedelta
import logging
import random
import re
import socket
import voluptuous as vol import voluptuous as vol
import homeassistant.components.hue as hue
import homeassistant.util as util import homeassistant.util as util
from homeassistant.util import yaml
import homeassistant.util.color as color_util import homeassistant.util.color as color_util
from homeassistant.components.light import ( from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH, ATTR_RGB_COLOR, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH, ATTR_RGB_COLOR,
@ -21,30 +23,17 @@ from homeassistant.components.light import (
FLASH_LONG, FLASH_SHORT, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, FLASH_LONG, FLASH_SHORT, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP,
SUPPORT_EFFECT, SUPPORT_FLASH, SUPPORT_RGB_COLOR, SUPPORT_TRANSITION, SUPPORT_EFFECT, SUPPORT_FLASH, SUPPORT_RGB_COLOR, SUPPORT_TRANSITION,
SUPPORT_XY_COLOR, Light, PLATFORM_SCHEMA) SUPPORT_XY_COLOR, Light, PLATFORM_SCHEMA)
from homeassistant.config import load_yaml_config_file from homeassistant.const import CONF_FILENAME, CONF_HOST, DEVICE_DEFAULT_NAME
from homeassistant.const import (CONF_FILENAME, CONF_HOST, DEVICE_DEFAULT_NAME)
from homeassistant.components.emulated_hue import ATTR_EMULATED_HUE_HIDDEN from homeassistant.components.emulated_hue import ATTR_EMULATED_HUE_HIDDEN
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['phue==1.0'] DEPENDENCIES = ['hue']
# Track previously setup bridges
_CONFIGURED_BRIDGES = {}
# Map ip to request id for configuring
_CONFIGURING = {}
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONF_ALLOW_UNREACHABLE = 'allow_unreachable'
DEFAULT_ALLOW_UNREACHABLE = False
DOMAIN = "light"
SERVICE_HUE_SCENE = "hue_activate_scene"
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(milliseconds=100) MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(milliseconds=100)
PHUE_CONFIG_FILE = 'phue.conf'
SUPPORT_HUE_ON_OFF = (SUPPORT_FLASH | SUPPORT_TRANSITION) SUPPORT_HUE_ON_OFF = (SUPPORT_FLASH | SUPPORT_TRANSITION)
SUPPORT_HUE_DIMMABLE = (SUPPORT_HUE_ON_OFF | SUPPORT_BRIGHTNESS) SUPPORT_HUE_DIMMABLE = (SUPPORT_HUE_ON_OFF | SUPPORT_BRIGHTNESS)
SUPPORT_HUE_COLOR_TEMP = (SUPPORT_HUE_DIMMABLE | SUPPORT_COLOR_TEMP) SUPPORT_HUE_COLOR_TEMP = (SUPPORT_HUE_DIMMABLE | SUPPORT_COLOR_TEMP)
@ -60,10 +49,14 @@ SUPPORT_HUE = {
'Color temperature light': SUPPORT_HUE_COLOR_TEMP 'Color temperature light': SUPPORT_HUE_COLOR_TEMP
} }
CONF_ALLOW_IN_EMULATED_HUE = "allow_in_emulated_hue" ATTR_IS_HUE_GROUP = 'is_hue_group'
DEFAULT_ALLOW_IN_EMULATED_HUE = True
CONF_ALLOW_HUE_GROUPS = "allow_hue_groups" # Legacy configuration, will be removed in 0.60
CONF_ALLOW_UNREACHABLE = 'allow_unreachable'
DEFAULT_ALLOW_UNREACHABLE = False
CONF_ALLOW_IN_EMULATED_HUE = 'allow_in_emulated_hue'
DEFAULT_ALLOW_IN_EMULATED_HUE = True
CONF_ALLOW_HUE_GROUPS = 'allow_hue_groups'
DEFAULT_ALLOW_HUE_GROUPS = True DEFAULT_ALLOW_HUE_GROUPS = True
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
@ -75,236 +68,158 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
default=DEFAULT_ALLOW_HUE_GROUPS): cv.boolean, default=DEFAULT_ALLOW_HUE_GROUPS): cv.boolean,
}) })
ATTR_GROUP_NAME = "group_name" MIGRATION_ID = 'light_hue_config_migration'
ATTR_SCENE_NAME = "scene_name" MIGRATION_TITLE = 'Philips Hue Configuration Migration'
SCENE_SCHEMA = vol.Schema({ MIGRATION_INSTRUCTIONS = """
vol.Required(ATTR_GROUP_NAME): cv.string, Configuration for the Philips Hue component has changed; action required.
vol.Required(ATTR_SCENE_NAME): cv.string,
})
ATTR_IS_HUE_GROUP = "is_hue_group" You have configured at least one bridge:
CONFIG_INSTRUCTIONS = """ hue:
Press the button on the bridge to register Philips Hue with Home Assistant. {config}
![Location of button on bridge](/static/images/config_philips_hue.jpg) This configuration is deprecated, please check the
[Hue component](https://home-assistant.io/components/hue/) page for more
information.
""" """
def _find_host_from_config(hass, filename=PHUE_CONFIG_FILE):
"""Attempt to detect host based on existing configuration."""
path = hass.config.path(filename)
if not os.path.isfile(path):
return None
try:
with open(path) as inp:
return next(json.loads(''.join(inp)).keys().__iter__())
except (ValueError, AttributeError, StopIteration):
# ValueError if can't parse as JSON
# AttributeError if JSON value is not a dict
# StopIteration if no keys
return None
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Hue lights.""" """Set up the Hue lights."""
# Default needed in case of discovery if discovery_info is None or 'bridge_id' not in discovery_info:
filename = config.get(CONF_FILENAME, PHUE_CONFIG_FILE)
allow_unreachable = config.get(CONF_ALLOW_UNREACHABLE,
DEFAULT_ALLOW_UNREACHABLE)
allow_in_emulated_hue = config.get(CONF_ALLOW_IN_EMULATED_HUE,
DEFAULT_ALLOW_IN_EMULATED_HUE)
allow_hue_groups = config.get(CONF_ALLOW_HUE_GROUPS)
if discovery_info is not None:
if "HASS Bridge" in discovery_info.get('name', ''):
_LOGGER.info("Emulated hue found, will not add")
return False
host = discovery_info.get('host')
else:
host = config.get(CONF_HOST, None)
if host is None:
host = _find_host_from_config(hass, filename)
if host is None:
_LOGGER.error("No host found in configuration")
return False
# Only act if we are not already configuring this host
if host in _CONFIGURING or \
socket.gethostbyname(host) in _CONFIGURED_BRIDGES:
return return
setup_bridge(host, hass, add_devices, filename, allow_unreachable, if config is not None and len(config) > 0:
allow_in_emulated_hue, allow_hue_groups) # Legacy configuration, will be removed in 0.60
config_str = yaml.dump([config])
# Indent so it renders in a fixed-width font
config_str = re.sub('(?m)^', ' ', config_str)
hass.components.persistent_notification.async_create(
MIGRATION_INSTRUCTIONS.format(config=config_str),
title=MIGRATION_TITLE,
notification_id=MIGRATION_ID)
bridge_id = discovery_info['bridge_id']
bridge = hass.data[hue.DOMAIN][bridge_id]
unthrottled_update_lights(hass, bridge, add_devices)
def setup_bridge(host, hass, add_devices, filename, allow_unreachable, @util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS)
allow_in_emulated_hue, allow_hue_groups): def update_lights(hass, bridge, add_devices):
"""Set up a phue bridge based on host parameter.""" """Update the Hue light objects with latest info from the bridge."""
return unthrottled_update_lights(hass, bridge, add_devices)
def unthrottled_update_lights(hass, bridge, add_devices):
"""Internal version of update_lights."""
import phue import phue
if not bridge.configured:
return
try: try:
bridge = phue.Bridge( api = bridge.get_api()
host, except phue.PhueRequestTimeout:
config_file_path=hass.config.path(filename)) _LOGGER.warning('Timeout trying to reach the bridge')
except ConnectionRefusedError: # Wrong host was given return
_LOGGER.error("Error connecting to the Hue bridge at %s", host) except ConnectionRefusedError:
_LOGGER.error('The bridge refused the connection')
return
except socket.error:
# socket.error when we cannot reach Hue
_LOGGER.exception('Cannot reach the bridge')
return return
except phue.PhueRegistrationException: bridge_type = get_bridge_type(api)
_LOGGER.warning("Connected to Hue at %s but not registered.", host)
request_configuration(host, hass, add_devices, filename, new_lights = process_lights(
allow_unreachable, allow_in_emulated_hue, hass, api, bridge, bridge_type,
allow_hue_groups) lambda **kw: update_lights(hass, bridge, add_devices, **kw))
if bridge.allow_hue_groups:
new_lightgroups = process_groups(
hass, api, bridge, bridge_type,
lambda **kw: update_lights(hass, bridge, add_devices, **kw))
new_lights.extend(new_lightgroups)
return if new_lights:
add_devices(new_lights)
# If we came here and configuring this host, mark as done
if host in _CONFIGURING:
request_id = _CONFIGURING.pop(host)
configurator = hass.components.configurator
configurator.request_done(request_id)
lights = {} def get_bridge_type(api):
lightgroups = {} """Return the bridge type."""
skip_groups = not allow_hue_groups api_name = api.get('config').get('name')
if api_name in ('RaspBee-GW', 'deCONZ-GW'):
return 'deconz'
else:
return 'hue'
@util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS)
def update_lights():
"""Update the Hue light objects with latest info from the bridge."""
nonlocal skip_groups
try: def process_lights(hass, api, bridge, bridge_type, update_lights_cb):
api = bridge.get_api() """Set up HueLight objects for all lights."""
except phue.PhueRequestTimeout: api_lights = api.get('lights')
_LOGGER.warning("Timeout trying to reach the bridge")
return
except ConnectionRefusedError:
_LOGGER.error("The bridge refused the connection")
return
except socket.error:
# socket.error when we cannot reach Hue
_LOGGER.exception("Cannot reach the bridge")
return
api_lights = api.get('lights') if not isinstance(api_lights, dict):
_LOGGER.error('Got unexpected result from Hue API')
return []
if not isinstance(api_lights, dict): new_lights = []
_LOGGER.error("Got unexpected result from Hue API")
return
if skip_groups: for light_id, info in api_lights.items():
api_groups = {} if light_id not in bridge.lights:
bridge.lights[light_id] = HueLight(
int(light_id), info, bridge,
update_lights_cb,
bridge_type, bridge.allow_unreachable,
bridge.allow_in_emulated_hue)
new_lights.append(bridge.lights[light_id])
else: else:
api_groups = api.get('groups') bridge.lights[light_id].info = info
bridge.lights[light_id].schedule_update_ha_state()
if not isinstance(api_groups, dict): return new_lights
_LOGGER.error("Got unexpected result from Hue API")
return
new_lights = []
api_name = api.get('config').get('name') def process_groups(hass, api, bridge, bridge_type, update_lights_cb):
if api_name in ('RaspBee-GW', 'deCONZ-GW'): """Set up HueLight objects for all groups."""
bridge_type = 'deconz' api_groups = api.get('groups')
if not isinstance(api_groups, dict):
_LOGGER.error('Got unexpected result from Hue API')
return []
new_lights = []
for lightgroup_id, info in api_groups.items():
if 'state' not in info:
_LOGGER.warning('Group info does not contain state. '
'Please update your hub.')
return []
if lightgroup_id not in bridge.lightgroups:
bridge.lightgroups[lightgroup_id] = HueLight(
int(lightgroup_id), info, bridge,
update_lights_cb,
bridge_type, bridge.allow_unreachable,
bridge.allow_in_emulated_hue, True)
new_lights.append(bridge.lightgroups[lightgroup_id])
else: else:
bridge_type = 'hue' bridge.lightgroups[lightgroup_id].info = info
bridge.lightgroups[lightgroup_id].schedule_update_ha_state()
for light_id, info in api_lights.items(): return new_lights
if light_id not in lights:
lights[light_id] = HueLight(int(light_id), info,
bridge, update_lights,
bridge_type, allow_unreachable,
allow_in_emulated_hue)
new_lights.append(lights[light_id])
else:
lights[light_id].info = info
lights[light_id].schedule_update_ha_state()
for lightgroup_id, info in api_groups.items():
if 'state' not in info:
_LOGGER.warning("Group info does not contain state. "
"Please update your hub.")
skip_groups = True
break
if lightgroup_id not in lightgroups:
lightgroups[lightgroup_id] = HueLight(
int(lightgroup_id), info, bridge, update_lights,
bridge_type, allow_unreachable, allow_in_emulated_hue,
True)
new_lights.append(lightgroups[lightgroup_id])
else:
lightgroups[lightgroup_id].info = info
lightgroups[lightgroup_id].schedule_update_ha_state()
if new_lights:
add_devices(new_lights)
_CONFIGURED_BRIDGES[socket.gethostbyname(host)] = True
# create a service for calling run_scene directly on the bridge,
# used to simplify automation rules.
def hue_activate_scene(call):
"""Service to call directly into bridge to set scenes."""
group_name = call.data[ATTR_GROUP_NAME]
scene_name = call.data[ATTR_SCENE_NAME]
bridge.run_scene(group_name, scene_name)
descriptions = load_yaml_config_file(
os.path.join(os.path.dirname(__file__), 'services.yaml'))
hass.services.register(DOMAIN, SERVICE_HUE_SCENE, hue_activate_scene,
descriptions.get(SERVICE_HUE_SCENE),
schema=SCENE_SCHEMA)
update_lights()
def request_configuration(host, hass, add_devices, filename,
allow_unreachable, allow_in_emulated_hue,
allow_hue_groups):
"""Request configuration steps from the user."""
configurator = hass.components.configurator
# We got an error if this method is called while we are configuring
if host in _CONFIGURING:
configurator.notify_errors(
_CONFIGURING[host], "Failed to register, please try again.")
return
# pylint: disable=unused-argument
def hue_configuration_callback(data):
"""Set up actions to do when our configuration callback is called."""
setup_bridge(host, hass, add_devices, filename, allow_unreachable,
allow_in_emulated_hue, allow_hue_groups)
_CONFIGURING[host] = configurator.request_config(
"Philips Hue", hue_configuration_callback,
description=CONFIG_INSTRUCTIONS,
entity_picture="/static/images/logo_philips_hue.png",
submit_caption="I have pressed the button"
)
class HueLight(Light): class HueLight(Light):
"""Representation of a Hue light.""" """Representation of a Hue light."""
def __init__(self, light_id, info, bridge, update_lights, def __init__(self, light_id, info, bridge, update_lights_cb,
bridge_type, allow_unreachable, allow_in_emulated_hue, bridge_type, allow_unreachable, allow_in_emulated_hue,
is_group=False): is_group=False):
"""Initialize the light.""" """Initialize the light."""
self.light_id = light_id self.light_id = light_id
self.info = info self.info = info
self.bridge = bridge self.bridge = bridge
self.update_lights = update_lights self.update_lights = update_lights_cb
self.bridge_type = bridge_type self.bridge_type = bridge_type
self.allow_unreachable = allow_unreachable self.allow_unreachable = allow_unreachable
self.is_group = is_group self.is_group = is_group
@ -381,14 +296,15 @@ class HueLight(Light):
command['transitiontime'] = int(kwargs[ATTR_TRANSITION] * 10) command['transitiontime'] = int(kwargs[ATTR_TRANSITION] * 10)
if ATTR_XY_COLOR in kwargs: if ATTR_XY_COLOR in kwargs:
if self.info.get('manufacturername') == "OSRAM": if self.info.get('manufacturername') == 'OSRAM':
hue, sat = color_util.color_xy_to_hs(*kwargs[ATTR_XY_COLOR]) color_hue, sat = color_util.color_xy_to_hs(
command['hue'] = hue *kwargs[ATTR_XY_COLOR])
command['hue'] = color_hue
command['sat'] = sat command['sat'] = sat
else: else:
command['xy'] = kwargs[ATTR_XY_COLOR] command['xy'] = kwargs[ATTR_XY_COLOR]
elif ATTR_RGB_COLOR in kwargs: elif ATTR_RGB_COLOR in kwargs:
if self.info.get('manufacturername') == "OSRAM": if self.info.get('manufacturername') == 'OSRAM':
hsv = color_util.color_RGB_to_hsv( hsv = color_util.color_RGB_to_hsv(
*(int(val) for val in kwargs[ATTR_RGB_COLOR])) *(int(val) for val in kwargs[ATTR_RGB_COLOR]))
command['hue'] = hsv[0] command['hue'] = hsv[0]

View File

@ -33,7 +33,7 @@ import homeassistant.util.color as color_util
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['aiolifx==0.6.0', 'aiolifx_effects==0.1.2'] REQUIREMENTS = ['aiolifx==0.6.1', 'aiolifx_effects==0.1.2']
UDP_BROADCAST_PORT = 56700 UDP_BROADCAST_PORT = 56700
@ -157,20 +157,10 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
return True return True
def lifxwhite(device): def lifx_features(device):
"""Return whether this is a white-only bulb.""" """Return a feature map for this device, or a default map if unknown."""
features = aiolifx().products.features_map.get(device.product, None) return aiolifx().products.features_map.get(device.product) or \
if features: aiolifx().products.features_map.get(1)
return not features["color"]
return False
def lifxmultizone(device):
"""Return whether this is a multizone bulb/strip."""
features = aiolifx().products.features_map.get(device.product, None)
if features:
return features["multizone"]
return False
def find_hsbk(**kwargs): def find_hsbk(**kwargs):
@ -342,12 +332,12 @@ class LIFXManager(object):
device.retry_count = MESSAGE_RETRIES device.retry_count = MESSAGE_RETRIES
device.unregister_timeout = UNAVAILABLE_GRACE device.unregister_timeout = UNAVAILABLE_GRACE
if lifxwhite(device): if lifx_features(device)["multizone"]:
entity = LIFXWhite(device, self.effects_conductor)
elif lifxmultizone(device):
entity = LIFXStrip(device, self.effects_conductor) entity = LIFXStrip(device, self.effects_conductor)
else: elif lifx_features(device)["color"]:
entity = LIFXColor(device, self.effects_conductor) entity = LIFXColor(device, self.effects_conductor)
else:
entity = LIFXWhite(device, self.effects_conductor)
_LOGGER.debug("%s register READY", entity.who) _LOGGER.debug("%s register READY", entity.who)
self.entities[device.mac_addr] = entity self.entities[device.mac_addr] = entity
@ -427,6 +417,29 @@ class LIFXLight(Light):
"""Return a string identifying the device.""" """Return a string identifying the device."""
return "%s (%s)" % (self.device.ip_addr, self.name) return "%s (%s)" % (self.device.ip_addr, self.name)
@property
def min_mireds(self):
"""Return the coldest color_temp that this light supports."""
kelvin = lifx_features(self.device)['max_kelvin']
return math.floor(color_util.color_temperature_kelvin_to_mired(kelvin))
@property
def max_mireds(self):
"""Return the warmest color_temp that this light supports."""
kelvin = lifx_features(self.device)['min_kelvin']
return math.ceil(color_util.color_temperature_kelvin_to_mired(kelvin))
@property
def supported_features(self):
"""Flag supported features."""
support = SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION | SUPPORT_EFFECT
device_features = lifx_features(self.device)
if device_features['min_kelvin'] != device_features['max_kelvin']:
support |= SUPPORT_COLOR_TEMP
return support
@property @property
def brightness(self): def brightness(self):
"""Return the brightness of this light between 0..255.""" """Return the brightness of this light between 0..255."""
@ -571,22 +584,6 @@ class LIFXLight(Light):
class LIFXWhite(LIFXLight): class LIFXWhite(LIFXLight):
"""Representation of a white-only LIFX light.""" """Representation of a white-only LIFX light."""
@property
def min_mireds(self):
"""Return the coldest color_temp that this light supports."""
return math.floor(color_util.color_temperature_kelvin_to_mired(6500))
@property
def max_mireds(self):
"""Return the warmest color_temp that this light supports."""
return math.ceil(color_util.color_temperature_kelvin_to_mired(2700))
@property
def supported_features(self):
"""Flag supported features."""
return (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_TRANSITION |
SUPPORT_EFFECT)
@property @property
def effect_list(self): def effect_list(self):
"""Return the list of supported effects for this light.""" """Return the list of supported effects for this light."""
@ -599,21 +596,12 @@ class LIFXWhite(LIFXLight):
class LIFXColor(LIFXLight): class LIFXColor(LIFXLight):
"""Representation of a color LIFX light.""" """Representation of a color LIFX light."""
@property
def min_mireds(self):
"""Return the coldest color_temp that this light supports."""
return math.floor(color_util.color_temperature_kelvin_to_mired(9000))
@property
def max_mireds(self):
"""Return the warmest color_temp that this light supports."""
return math.ceil(color_util.color_temperature_kelvin_to_mired(2500))
@property @property
def supported_features(self): def supported_features(self):
"""Flag supported features.""" """Flag supported features."""
return (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_TRANSITION | support = super().supported_features
SUPPORT_EFFECT | SUPPORT_RGB_COLOR | SUPPORT_XY_COLOR) support |= SUPPORT_RGB_COLOR | SUPPORT_XY_COLOR
return support
@property @property
def effect_list(self): def effect_list(self):

View File

@ -12,13 +12,15 @@ import voluptuous as vol
from homeassistant.components.light import ( from homeassistant.components.light import (
ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light, PLATFORM_SCHEMA) ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light, PLATFORM_SCHEMA)
from homeassistant.components import mochad from homeassistant.components import mochad
from homeassistant.const import (CONF_NAME, CONF_PLATFORM, CONF_DEVICES, from homeassistant.const import (
CONF_ADDRESS) CONF_NAME, CONF_PLATFORM, CONF_DEVICES, CONF_ADDRESS)
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
DEPENDENCIES = ['mochad'] DEPENDENCIES = ['mochad']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONF_BRIGHTNESS_LEVELS = 'brightness_levels'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_PLATFORM): mochad.DOMAIN, vol.Required(CONF_PLATFORM): mochad.DOMAIN,
@ -26,6 +28,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_NAME): cv.string,
vol.Required(CONF_ADDRESS): cv.x10_address, vol.Required(CONF_ADDRESS): cv.x10_address,
vol.Optional(mochad.CONF_COMM_TYPE): cv.string, vol.Optional(mochad.CONF_COMM_TYPE): cv.string,
vol.Optional(CONF_BRIGHTNESS_LEVELS, default=32):
vol.All(vol.Coerce(int), vol.In([32, 64, 256])),
}] }]
}) })
@ -54,6 +58,7 @@ class MochadLight(Light):
comm_type=self._comm_type) comm_type=self._comm_type)
self._brightness = 0 self._brightness = 0
self._state = self._get_device_status() self._state = self._get_device_status()
self._brightness_levels = dev.get(CONF_BRIGHTNESS_LEVELS) - 1
@property @property
def brightness(self): def brightness(self):
@ -62,7 +67,8 @@ class MochadLight(Light):
def _get_device_status(self): def _get_device_status(self):
"""Get the status of the light from mochad.""" """Get the status of the light from mochad."""
status = self.device.get_status().rstrip() with mochad.REQ_LOCK:
status = self.device.get_status().rstrip()
return status == 'on' return status == 'on'
@property @property
@ -85,15 +91,47 @@ class MochadLight(Light):
"""X10 devices are normally 1-way so we have to assume the state.""" """X10 devices are normally 1-way so we have to assume the state."""
return True return True
def _calculate_brightness_value(self, value):
return int(value * (float(self._brightness_levels) / 255.0))
def _adjust_brightness(self, brightness):
if self._brightness > brightness:
bdelta = self._brightness - brightness
mochad_brightness = self._calculate_brightness_value(bdelta)
self.device.send_cmd("dim {}".format(mochad_brightness))
self._controller.read_data()
elif self._brightness < brightness:
bdelta = brightness - self._brightness
mochad_brightness = self._calculate_brightness_value(bdelta)
self.device.send_cmd("bright {}".format(mochad_brightness))
self._controller.read_data()
def turn_on(self, **kwargs): def turn_on(self, **kwargs):
"""Send the command to turn the light on.""" """Send the command to turn the light on."""
self._brightness = kwargs.get(ATTR_BRIGHTNESS, 255) brightness = kwargs.get(ATTR_BRIGHTNESS, 255)
self.device.send_cmd("xdim {}".format(self._brightness)) with mochad.REQ_LOCK:
self._controller.read_data() if self._brightness_levels > 32:
out_brightness = self._calculate_brightness_value(brightness)
self.device.send_cmd('xdim {}'.format(out_brightness))
self._controller.read_data()
else:
self.device.send_cmd("on")
self._controller.read_data()
# There is no persistence for X10 modules so a fresh on command
# will be full brightness
if self._brightness == 0:
self._brightness = 255
self._adjust_brightness(brightness)
self._brightness = brightness
self._state = True self._state = True
def turn_off(self, **kwargs): def turn_off(self, **kwargs):
"""Send the command to turn the light on.""" """Send the command to turn the light on."""
self.device.send_cmd('off') with mochad.REQ_LOCK:
self._controller.read_data() self.device.send_cmd('off')
self._controller.read_data()
# There is no persistence for X10 modules so we need to prepare
# to track a fresh on command will full brightness
if self._brightness_levels == 31:
self._brightness = 0
self._state = False self._state = False

View File

@ -72,6 +72,7 @@ class TPLinkSmartBulb(Light):
if name is not None: if name is not None:
self._name = name self._name = name
self._state = None self._state = None
self._available = True
self._color_temp = None self._color_temp = None
self._brightness = None self._brightness = None
self._rgb = None self._rgb = None
@ -83,6 +84,11 @@ class TPLinkSmartBulb(Light):
"""Return the name of the Smart Bulb, if any.""" """Return the name of the Smart Bulb, if any."""
return self._name return self._name
@property
def available(self) -> bool:
"""Return if bulb is available."""
return self._available
@property @property
def device_state_attributes(self): def device_state_attributes(self):
"""Return the state attributes of the device.""" """Return the state attributes of the device."""
@ -132,6 +138,7 @@ class TPLinkSmartBulb(Light):
"""Update the TP-Link Bulb's state.""" """Update the TP-Link Bulb's state."""
from pyHS100 import SmartDeviceException from pyHS100 import SmartDeviceException
try: try:
self._available = True
if self._supported_features == 0: if self._supported_features == 0:
self.get_features() self.get_features()
self._state = ( self._state = (
@ -163,8 +170,10 @@ class TPLinkSmartBulb(Light):
except KeyError: except KeyError:
# device returned no daily/monthly history # device returned no daily/monthly history
pass pass
except (SmartDeviceException, OSError) as ex: except (SmartDeviceException, OSError) as ex:
_LOGGER.warning('Could not read state for %s: %s', self._name, ex) _LOGGER.warning("Could not read state for %s: %s", self._name, ex)
self._available = False
@property @property
def supported_features(self): def supported_features(self):

View File

@ -160,6 +160,7 @@ class TradfriLight(Light):
self._rgb_color = None self._rgb_color = None
self._features = SUPPORTED_FEATURES self._features = SUPPORTED_FEATURES
self._temp_supported = False self._temp_supported = False
self._available = True
self._refresh(light) self._refresh(light)
@ -196,6 +197,11 @@ class TradfriLight(Light):
"""Start thread when added to hass.""" """Start thread when added to hass."""
self._async_start_observe() self._async_start_observe()
@property
def available(self):
"""Return True if entity is available."""
return self._available
@property @property
def should_poll(self): def should_poll(self):
"""No polling needed for tradfri light.""" """No polling needed for tradfri light."""
@ -299,6 +305,7 @@ class TradfriLight(Light):
self._light = light self._light = light
# Caching of LightControl and light object # Caching of LightControl and light object
self._available = light.reachable
self._light_control = light.light_control self._light_control = light.light_control
self._light_data = light.light_control.lights[0] self._light_data = light.light_control.lights[0]
self._name = light.name self._name = light.name

View File

@ -21,7 +21,8 @@ DEPENDENCIES = ['vera']
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Vera lights.""" """Set up the Vera lights."""
add_devices( add_devices(
VeraLight(device, VERA_CONTROLLER) for device in VERA_DEVICES['light']) VeraLight(device, hass.data[VERA_CONTROLLER]) for
device in hass.data[VERA_DEVICES]['light'])
class VeraLight(VeraDevice, Light): class VeraLight(VeraDevice, Light):

View File

@ -28,7 +28,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
}) })
REQUIREMENTS = ['python-miio==0.3.2'] REQUIREMENTS = ['python-miio==0.3.3']
# The light does not accept cct values < 1 # The light does not accept cct values < 1
CCT_MIN = 1 CCT_MIN = 1

View File

@ -66,7 +66,10 @@ class ISYLockDevice(isy.ISYDevice, LockDevice):
@property @property
def state(self) -> str: def state(self) -> str:
"""Get the state of the lock.""" """Get the state of the lock."""
return VALUE_TO_STATE.get(self.value, STATE_UNKNOWN) if self.is_unknown():
return None
else:
return VALUE_TO_STATE.get(self.value, STATE_UNKNOWN)
def lock(self, **kwargs) -> None: def lock(self, **kwargs) -> None:
"""Send the lock command to the ISY994 device.""" """Send the lock command to the ISY994 device."""

View File

@ -25,46 +25,53 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
# pylint: disable=unused-argument # pylint: disable=unused-argument
def setup_platform(hass, config: ConfigType, def setup_platform(
add_devices: Callable[[list], None], discovery_info=None): hass, config: ConfigType,
add_devices: Callable[[list], None], discovery_info=None):
"""Set up the Sesame platform.""" """Set up the Sesame platform."""
import pysesame import pysesame
email = config.get(CONF_EMAIL) email = config.get(CONF_EMAIL)
password = config.get(CONF_PASSWORD) password = config.get(CONF_PASSWORD)
add_devices([SesameDevice(sesame) for add_devices([SesameDevice(sesame) for sesame in
sesame in pysesame.get_sesames(email, password)]) pysesame.get_sesames(email, password)],
update_before_add=True)
class SesameDevice(LockDevice): class SesameDevice(LockDevice):
"""Representation of a Sesame device.""" """Representation of a Sesame device."""
_sesame = None
def __init__(self, sesame: object) -> None: def __init__(self, sesame: object) -> None:
"""Initialize the Sesame device.""" """Initialize the Sesame device."""
self._sesame = sesame self._sesame = sesame
# Cached properties from pysesame object.
self._device_id = None
self._nickname = None
self._is_unlocked = False
self._api_enabled = False
self._battery = -1
@property @property
def name(self) -> str: def name(self) -> str:
"""Return the name of the device.""" """Return the name of the device."""
return self._sesame.nickname return self._nickname
@property @property
def available(self) -> bool: def available(self) -> bool:
"""Return True if entity is available.""" """Return True if entity is available."""
return self._sesame.api_enabled return self._api_enabled
@property @property
def is_locked(self) -> bool: def is_locked(self) -> bool:
"""Return True if the device is currently locked, else False.""" """Return True if the device is currently locked, else False."""
return not self._sesame.is_unlocked return not self._is_unlocked
@property @property
def state(self) -> str: def state(self) -> str:
"""Get the state of the device.""" """Get the state of the device."""
if self._sesame.is_unlocked: if self._is_unlocked:
return STATE_UNLOCKED return STATE_UNLOCKED
return STATE_LOCKED return STATE_LOCKED
@ -79,11 +86,16 @@ class SesameDevice(LockDevice):
def update(self) -> None: def update(self) -> None:
"""Update the internal state of the device.""" """Update the internal state of the device."""
self._sesame.update_state() self._sesame.update_state()
self._nickname = self._sesame.nickname
self._api_enabled = self._sesame.api_enabled
self._is_unlocked = self._sesame.is_unlocked
self._device_id = self._sesame.device_id
self._battery = self._sesame.battery
@property @property
def device_state_attributes(self) -> dict: def device_state_attributes(self) -> dict:
"""Return the state attributes.""" """Return the state attributes."""
attributes = {} attributes = {}
attributes[ATTR_DEVICE_ID] = self._sesame.device_id attributes[ATTR_DEVICE_ID] = self._device_id
attributes[ATTR_BATTERY_LEVEL] = self._sesame.battery attributes[ATTR_BATTERY_LEVEL] = self._battery
return attributes return attributes

View File

@ -19,8 +19,8 @@ DEPENDENCIES = ['vera']
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Find and return Vera locks.""" """Find and return Vera locks."""
add_devices( add_devices(
VeraLock(device, VERA_CONTROLLER) for VeraLock(device, hass.data[VERA_CONTROLLER]) for
device in VERA_DEVICES['lock']) device in hass.data[VERA_DEVICES]['lock'])
class VeraLock(VeraDevice, LockDevice): class VeraLock(VeraDevice, LockDevice):

View File

@ -135,9 +135,8 @@ class LogbookView(HomeAssistantView):
hass = request.app['hass'] hass = request.app['hass']
events = yield from hass.async_add_job( events = yield from hass.async_add_job(
_get_events, hass, start_day, end_day) _get_events, hass, self.config, start_day, end_day)
events = _exclude_events(events, self.config) return self.json(events)
return self.json(humanify(events))
class Entry(object): class Entry(object):
@ -274,7 +273,7 @@ def humanify(events):
entity_id) entity_id)
def _get_events(hass, start_day, end_day): def _get_events(hass, config, start_day, end_day):
"""Get events for a period of time.""" """Get events for a period of time."""
from homeassistant.components.recorder.models import Events from homeassistant.components.recorder.models import Events
from homeassistant.components.recorder.util import ( from homeassistant.components.recorder.util import (
@ -285,7 +284,8 @@ def _get_events(hass, start_day, end_day):
Events.time_fired).filter( Events.time_fired).filter(
(Events.time_fired > start_day) & (Events.time_fired > start_day) &
(Events.time_fired < end_day)) (Events.time_fired < end_day))
return execute(query) events = execute(query)
return humanify(_exclude_events(events, config))
def _exclude_events(events, config): def _exclude_events(events, config):

View File

@ -16,7 +16,7 @@ from homeassistant.components.media_player import (
from homeassistant.config import load_yaml_config_file from homeassistant.config import load_yaml_config_file
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
REQUIREMENTS = ['youtube_dl==2017.11.26'] REQUIREMENTS = ['youtube_dl==2017.12.10']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -20,8 +20,9 @@ from homeassistant.const import (
CONF_HOST, CONF_PORT, STATE_ON, STATE_OFF, STATE_PLAYING, CONF_HOST, CONF_PORT, STATE_ON, STATE_OFF, STATE_PLAYING,
STATE_PAUSED, CONF_NAME) STATE_PAUSED, CONF_NAME)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
import homeassistant.util.dt as dt_util
REQUIREMENTS = ['liveboxplaytv==2.0.0'] REQUIREMENTS = ['liveboxplaytv==2.0.2', 'pyteleloisirs==3.3']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -76,19 +77,32 @@ class LiveboxPlayTvDevice(MediaPlayerDevice):
self._channel_list = {} self._channel_list = {}
self._current_channel = None self._current_channel = None
self._current_program = None self._current_program = None
self._media_duration = None
self._media_remaining_time = None
self._media_image_url = None self._media_image_url = None
self._media_last_updated = None
@asyncio.coroutine @asyncio.coroutine
def async_update(self): def async_update(self):
"""Retrieve the latest data.""" """Retrieve the latest data."""
import pyteleloisirs
try: try:
self._state = self.refresh_state() self._state = self.refresh_state()
# Update current channel # Update current channel
channel = self._client.channel channel = self._client.channel
if channel is not None: if channel is not None:
self._current_program = yield from \
self._client.async_get_current_program_name()
self._current_channel = channel self._current_channel = channel
program = yield from \
self._client.async_get_current_program()
if program and self._current_program != program.get('name'):
self._current_program = program.get('name')
# Media progress info
self._media_duration = \
pyteleloisirs.get_program_duration(program)
rtime = pyteleloisirs.get_remaining_time(program)
if rtime != self._media_remaining_time:
self._media_remaining_time = rtime
self._media_last_updated = dt_util.utcnow()
# Set media image to current program if a thumbnail is # Set media image to current program if a thumbnail is
# available. Otherwise we'll use the channel's image. # available. Otherwise we'll use the channel's image.
img_size = 800 img_size = 800
@ -100,7 +114,6 @@ class LiveboxPlayTvDevice(MediaPlayerDevice):
chan_img_url = \ chan_img_url = \
self._client.get_current_channel_image(img_size) self._client.get_current_channel_image(img_size)
self._media_image_url = chan_img_url self._media_image_url = chan_img_url
self.refresh_channel_list()
except requests.ConnectionError: except requests.ConnectionError:
self._state = None self._state = None
@ -149,8 +162,25 @@ class LiveboxPlayTvDevice(MediaPlayerDevice):
if self._current_program: if self._current_program:
return '{}: {}'.format(self._current_channel, return '{}: {}'.format(self._current_channel,
self._current_program) self._current_program)
else: return self._current_channel
return self._current_channel
@property
def media_duration(self):
"""Duration of current playing media in seconds."""
return self._media_duration
@property
def media_position(self):
"""Position of current playing media in seconds."""
return self._media_remaining_time
@property
def media_position_updated_at(self):
"""When was the position of the current playing media valid.
Returns value from homeassistant.util.dt.utcnow().
"""
return self._media_last_updated
@property @property
def supported_features(self): def supported_features(self):

View File

@ -5,18 +5,21 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/media_player.monoprice/ https://home-assistant.io/components/media_player.monoprice/
""" """
import logging import logging
from os import path
import voluptuous as vol import voluptuous as vol
from homeassistant.const import (CONF_NAME, CONF_PORT, STATE_OFF, STATE_ON) from homeassistant.const import (ATTR_ENTITY_ID, CONF_NAME, CONF_PORT,
STATE_OFF, STATE_ON)
from homeassistant.config import load_yaml_config_file
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.components.media_player import ( from homeassistant.components.media_player import (
MediaPlayerDevice, PLATFORM_SCHEMA, SUPPORT_VOLUME_MUTE, DOMAIN, MediaPlayerDevice, MEDIA_PLAYER_SCHEMA, PLATFORM_SCHEMA,
SUPPORT_SELECT_SOURCE, SUPPORT_TURN_ON, SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE, SUPPORT_SELECT_SOURCE, SUPPORT_TURN_ON,
SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP) SUPPORT_TURN_OFF, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP)
REQUIREMENTS = ['pymonoprice==0.2'] REQUIREMENTS = ['pymonoprice==0.3']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -35,6 +38,11 @@ SOURCE_SCHEMA = vol.Schema({
CONF_ZONES = 'zones' CONF_ZONES = 'zones'
CONF_SOURCES = 'sources' CONF_SOURCES = 'sources'
DATA_MONOPRICE = 'monoprice'
SERVICE_SNAPSHOT = 'snapshot'
SERVICE_RESTORE = 'restore'
# Valid zone ids: 11-16 or 21-26 or 31-36 # Valid zone ids: 11-16 or 21-26 or 31-36
ZONE_IDS = vol.All(vol.Coerce(int), vol.Any(vol.Range(min=11, max=16), ZONE_IDS = vol.All(vol.Coerce(int), vol.Any(vol.Range(min=11, max=16),
vol.Range(min=21, max=26), vol.Range(min=21, max=26),
@ -56,9 +64,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
port = config.get(CONF_PORT) port = config.get(CONF_PORT)
from serial import SerialException from serial import SerialException
from pymonoprice import Monoprice from pymonoprice import get_monoprice
try: try:
monoprice = Monoprice(port) monoprice = get_monoprice(port)
except SerialException: except SerialException:
_LOGGER.error('Error connecting to Monoprice controller.') _LOGGER.error('Error connecting to Monoprice controller.')
return return
@ -66,10 +74,41 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
sources = {source_id: extra[CONF_NAME] for source_id, extra sources = {source_id: extra[CONF_NAME] for source_id, extra
in config[CONF_SOURCES].items()} in config[CONF_SOURCES].items()}
hass.data[DATA_MONOPRICE] = []
for zone_id, extra in config[CONF_ZONES].items(): for zone_id, extra in config[CONF_ZONES].items():
_LOGGER.info("Adding zone %d - %s", zone_id, extra[CONF_NAME]) _LOGGER.info("Adding zone %d - %s", zone_id, extra[CONF_NAME])
add_devices([MonopriceZone(monoprice, sources, hass.data[DATA_MONOPRICE].append(MonopriceZone(monoprice, sources,
zone_id, extra[CONF_NAME])], True) zone_id,
extra[CONF_NAME]))
add_devices(hass.data[DATA_MONOPRICE], True)
descriptions = load_yaml_config_file(
path.join(path.dirname(__file__), 'services.yaml'))
def service_handle(service):
"""Handle for services."""
entity_ids = service.data.get(ATTR_ENTITY_ID)
if entity_ids:
devices = [device for device in hass.data[DATA_MONOPRICE]
if device.entity_id in entity_ids]
else:
devices = hass.data[DATA_MONOPRICE]
for device in devices:
if service.service == SERVICE_SNAPSHOT:
device.snapshot()
elif service.service == SERVICE_RESTORE:
device.restore()
hass.services.register(
DOMAIN, SERVICE_SNAPSHOT, service_handle,
descriptions.get(SERVICE_SNAPSHOT), schema=MEDIA_PLAYER_SCHEMA)
hass.services.register(
DOMAIN, SERVICE_RESTORE, service_handle,
descriptions.get(SERVICE_RESTORE), schema=MEDIA_PLAYER_SCHEMA)
class MonopriceZone(MediaPlayerDevice): class MonopriceZone(MediaPlayerDevice):
@ -90,6 +129,7 @@ class MonopriceZone(MediaPlayerDevice):
self._zone_id = zone_id self._zone_id = zone_id
self._name = zone_name self._name = zone_name
self._snapshot = None
self._state = None self._state = None
self._volume = None self._volume = None
self._source = None self._source = None
@ -152,6 +192,16 @@ class MonopriceZone(MediaPlayerDevice):
"""List of available input sources.""" """List of available input sources."""
return self._source_names return self._source_names
def snapshot(self):
"""Save zone's current state."""
self._snapshot = self._monoprice.zone_status(self._zone_id)
def restore(self):
"""Restore saved state."""
if self._snapshot:
self._monoprice.restore_zone(self._snapshot)
self.schedule_update_ha_state(True)
def select_source(self, source): def select_source(self, source):
"""Set input source.""" """Set input source."""
if source not in self._source_name_id: if source not in self._source_name_id:

View File

@ -227,7 +227,7 @@ def request_configuration(host, hass, config, add_devices_callback):
_CONFIGURING[host] = configurator.request_config( _CONFIGURING[host] = configurator.request_config(
'Plex Media Server', 'Plex Media Server',
plex_configuration_callback, plex_configuration_callback,
description=('Enter the X-Plex-Token'), description='Enter the X-Plex-Token',
entity_picture='/static/images/logo_plex_mediaserver.png', entity_picture='/static/images/logo_plex_mediaserver.png',
submit_caption='Confirm', submit_caption='Confirm',
fields=[{ fields=[{
@ -273,8 +273,23 @@ class PlexClient(MediaPlayerDevice):
self.plex_sessions = plex_sessions self.plex_sessions = plex_sessions
self.update_devices = update_devices self.update_devices = update_devices
self.update_sessions = update_sessions self.update_sessions = update_sessions
# General
self._clear_media() self._media_content_id = None
self._media_content_rating = None
self._media_content_type = None
self._media_duration = None
self._media_image_url = None
self._media_title = None
self._media_position = None
# Music
self._media_album_artist = None
self._media_album_name = None
self._media_artist = None
self._media_track = None
# TV Show
self._media_episode = None
self._media_season = None
self._media_series_title = None
self.refresh(device, session) self.refresh(device, session)
@ -296,7 +311,7 @@ class PlexClient(MediaPlayerDevice):
'media_player', prefix, 'media_player', prefix,
self.name.lower().replace('-', '_')) self.name.lower().replace('-', '_'))
def _clear_media(self): def _clear_media_details(self):
"""Set all Media Items to None.""" """Set all Media Items to None."""
# General # General
self._media_content_id = None self._media_content_id = None
@ -316,10 +331,13 @@ class PlexClient(MediaPlayerDevice):
self._media_season = None self._media_season = None
self._media_series_title = None self._media_series_title = None
# Clear library Name
self._app_name = ''
def refresh(self, device, session): def refresh(self, device, session):
"""Refresh key device data.""" """Refresh key device data."""
# new data refresh # new data refresh
self._clear_media() self._clear_media_details()
if session: # Not being triggered by Chrome or FireTablet Plex App if session: # Not being triggered by Chrome or FireTablet Plex App
self._session = session self._session = session
@ -355,6 +373,36 @@ class PlexClient(MediaPlayerDevice):
self._media_content_id = self._session.ratingKey self._media_content_id = self._session.ratingKey
self._media_content_rating = self._session.contentRating self._media_content_rating = self._session.contentRating
self._set_player_state()
if self._is_player_active and self._session is not None:
self._session_type = self._session.type
self._media_duration = self._session.duration
# title (movie name, tv episode name, music song name)
self._media_title = self._session.title
# media type
self._set_media_type()
self._app_name = self._session.section().title \
if self._session.section() is not None else ''
self._set_media_image()
else:
self._session_type = None
def _set_media_image(self):
thumb_url = self._session.thumbUrl
if (self.media_content_type is MEDIA_TYPE_TVSHOW
and not self.config.get(CONF_USE_EPISODE_ART)):
thumb_url = self._server.url(
self._session.grandparentThumb)
if thumb_url is None:
_LOGGER.debug("Using media art because media thumb "
"was not found: %s", self.entity_id)
thumb_url = self._server.url(self._session.art)
self._media_image_url = thumb_url
def _set_player_state(self):
if self._player_state == 'playing': if self._player_state == 'playing':
self._is_player_active = True self._is_player_active = True
self._state = STATE_PLAYING self._state = STATE_PLAYING
@ -368,35 +416,10 @@ class PlexClient(MediaPlayerDevice):
self._is_player_active = False self._is_player_active = False
self._state = STATE_OFF self._state = STATE_OFF
if self._is_player_active and self._session is not None: def _set_media_type(self):
self._session_type = self._session.type if self._session_type in ['clip', 'episode']:
self._media_duration = self._session.duration
else:
self._session_type = None
# media type
if self._session_type == 'clip':
_LOGGER.debug("Clip content type detected, compatibility may "
"vary: %s", self.entity_id)
self._media_content_type = MEDIA_TYPE_TVSHOW self._media_content_type = MEDIA_TYPE_TVSHOW
elif self._session_type == 'episode':
self._media_content_type = MEDIA_TYPE_TVSHOW
elif self._session_type == 'movie':
self._media_content_type = MEDIA_TYPE_VIDEO
elif self._session_type == 'track':
self._media_content_type = MEDIA_TYPE_MUSIC
# title (movie name, tv episode name, music song name)
if self._session and self._is_player_active:
self._media_title = self._session.title
# Movies
if (self.media_content_type == MEDIA_TYPE_VIDEO and
self._session.year is not None):
self._media_title += ' (' + str(self._session.year) + ')'
# TV Show
if self._media_content_type is MEDIA_TYPE_TVSHOW:
# season number (00) # season number (00)
if callable(self._session.seasons): if callable(self._session.seasons):
self._media_season = self._session.seasons()[0].index.zfill(2) self._media_season = self._session.seasons()[0].index.zfill(2)
@ -410,8 +433,14 @@ class PlexClient(MediaPlayerDevice):
if self._session.index is not None: if self._session.index is not None:
self._media_episode = str(self._session.index).zfill(2) self._media_episode = str(self._session.index).zfill(2)
# Music elif self._session_type == 'movie':
if self._media_content_type == MEDIA_TYPE_MUSIC: self._media_content_type = MEDIA_TYPE_VIDEO
if self._session.year is not None and \
self._media_title is not None:
self._media_title += ' (' + str(self._session.year) + ')'
elif self._session_type == 'track':
self._media_content_type = MEDIA_TYPE_MUSIC
self._media_album_name = self._session.parentTitle self._media_album_name = self._session.parentTitle
self._media_album_artist = self._session.grandparentTitle self._media_album_artist = self._session.grandparentTitle
self._media_track = self._session.index self._media_track = self._session.index
@ -422,33 +451,11 @@ class PlexClient(MediaPlayerDevice):
"was not found: %s", self.entity_id) "was not found: %s", self.entity_id)
self._media_artist = self._media_album_artist self._media_artist = self._media_album_artist
# set app name to library name
if (self._session is not None
and self._session.section() is not None):
self._app_name = self._session.section().title
else:
self._app_name = ''
# media image url
if self._session is not None:
thumb_url = self._session.thumbUrl
if (self.media_content_type is MEDIA_TYPE_TVSHOW
and not self.config.get(CONF_USE_EPISODE_ART)):
thumb_url = self._server.url(
self._session.grandparentThumb)
if thumb_url is None:
_LOGGER.debug("Using media art because media thumb "
"was not found: %s", self.entity_id)
thumb_url = self._server.url(self._session.art)
self._media_image_url = thumb_url
def force_idle(self): def force_idle(self):
"""Force client to idle.""" """Force client to idle."""
self._state = STATE_IDLE self._state = STATE_IDLE
self._session = None self._session = None
self._clear_media() self._clear_media_details()
@property @property
def unique_id(self): def unique_id(self):
@ -792,9 +799,10 @@ class PlexClient(MediaPlayerDevice):
@property @property
def device_state_attributes(self): def device_state_attributes(self):
"""Return the scene state attributes.""" """Return the scene state attributes."""
attr = {} attr = {
attr['media_content_rating'] = self._media_content_rating 'media_content_rating': self._media_content_rating,
attr['session_username'] = self._session_username 'session_username': self._session_username,
attr['media_library_name'] = self._app_name 'media_library_name': self._app_name
}
return attr return attr

View File

@ -190,7 +190,10 @@ class SamsungTVDevice(MediaPlayerDevice):
else: else:
self.send_key('KEY_POWEROFF') self.send_key('KEY_POWEROFF')
# Force closing of remote session to provide instant UI feedback # Force closing of remote session to provide instant UI feedback
self.get_remote().close() try:
self.get_remote().close()
except OSError:
_LOGGER.debug("Could not establish connection.")
def volume_up(self): def volume_up(self):
"""Volume up the media player.""" """Volume up the media player."""

View File

@ -107,6 +107,20 @@ media_seek:
description: Position to seek to. The format is platform dependent. description: Position to seek to. The format is platform dependent.
example: 100 example: 100
monoprice_snapshot:
description: Take a snapshot of the media player zone.
fields:
entity_id:
description: Name(s) of entities that will be snapshot. Platform dependent.
example: 'media_player.living_room'
monoprice_restore:
description: Restore a snapshot of the media player zone.
fields:
entity_id:
description: Name(s) of entities that will be restored. Platform dependent.
example: 'media_player.living_room'
play_media: play_media:
description: Send the media player the command for playing media. description: Send the media player the command for playing media.
fields: fields:
@ -215,6 +229,18 @@ sonos_clear_sleep_timer:
description: Name(s) of entities that will have the timer cleared. description: Name(s) of entities that will have the timer cleared.
example: 'media_player.living_room_sonos' example: 'media_player.living_room_sonos'
sonos_set_option:
description: Set Sonos sound options.
fields:
entity_id:
description: Name(s) of entities that will have options set.
example: 'media_player.living_room_sonos'
night_sound:
description: Enable Night Sound mode
example: 'true'
speech_enhance:
description: Enable Speech Enhancement mode
example: 'true'
soundtouch_play_everywhere: soundtouch_play_everywhere:
description: Play on all Bose Soundtouch devices. description: Play on all Bose Soundtouch devices.

View File

@ -19,7 +19,7 @@ from homeassistant.components.media_player import (
SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK, SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK,
SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_CLEAR_PLAYLIST, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_CLEAR_PLAYLIST,
SUPPORT_SELECT_SOURCE, MediaPlayerDevice, PLATFORM_SCHEMA, SUPPORT_STOP, SUPPORT_SELECT_SOURCE, MediaPlayerDevice, PLATFORM_SCHEMA, SUPPORT_STOP,
SUPPORT_PLAY) SUPPORT_PLAY, SUPPORT_SHUFFLE_SET)
from homeassistant.const import ( from homeassistant.const import (
STATE_IDLE, STATE_PAUSED, STATE_PLAYING, STATE_OFF, ATTR_ENTITY_ID, STATE_IDLE, STATE_PAUSED, STATE_PLAYING, STATE_OFF, ATTR_ENTITY_ID,
CONF_HOSTS, ATTR_TIME) CONF_HOSTS, ATTR_TIME)
@ -27,7 +27,7 @@ from homeassistant.config import load_yaml_config_file
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.util.dt import utcnow from homeassistant.util.dt import utcnow
REQUIREMENTS = ['SoCo==0.12'] REQUIREMENTS = ['SoCo==0.13']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -43,7 +43,7 @@ _REQUESTS_LOGGER.setLevel(logging.ERROR)
SUPPORT_SONOS = SUPPORT_STOP | SUPPORT_PAUSE | SUPPORT_VOLUME_SET |\ SUPPORT_SONOS = SUPPORT_STOP | SUPPORT_PAUSE | SUPPORT_VOLUME_SET |\
SUPPORT_VOLUME_MUTE | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK |\ SUPPORT_VOLUME_MUTE | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK |\
SUPPORT_PLAY_MEDIA | SUPPORT_SEEK | SUPPORT_CLEAR_PLAYLIST |\ SUPPORT_PLAY_MEDIA | SUPPORT_SEEK | SUPPORT_CLEAR_PLAYLIST |\
SUPPORT_SELECT_SOURCE | SUPPORT_PLAY SUPPORT_SELECT_SOURCE | SUPPORT_PLAY | SUPPORT_SHUFFLE_SET
SERVICE_JOIN = 'sonos_join' SERVICE_JOIN = 'sonos_join'
SERVICE_UNJOIN = 'sonos_unjoin' SERVICE_UNJOIN = 'sonos_unjoin'
@ -52,6 +52,7 @@ SERVICE_RESTORE = 'sonos_restore'
SERVICE_SET_TIMER = 'sonos_set_sleep_timer' SERVICE_SET_TIMER = 'sonos_set_sleep_timer'
SERVICE_CLEAR_TIMER = 'sonos_clear_sleep_timer' SERVICE_CLEAR_TIMER = 'sonos_clear_sleep_timer'
SERVICE_UPDATE_ALARM = 'sonos_update_alarm' SERVICE_UPDATE_ALARM = 'sonos_update_alarm'
SERVICE_SET_OPTION = 'sonos_set_option'
DATA_SONOS = 'sonos' DATA_SONOS = 'sonos'
@ -69,6 +70,8 @@ ATTR_ENABLED = 'enabled'
ATTR_INCLUDE_LINKED_ZONES = 'include_linked_zones' ATTR_INCLUDE_LINKED_ZONES = 'include_linked_zones'
ATTR_MASTER = 'master' ATTR_MASTER = 'master'
ATTR_WITH_GROUP = 'with_group' ATTR_WITH_GROUP = 'with_group'
ATTR_NIGHT_SOUND = 'night_sound'
ATTR_SPEECH_ENHANCE = 'speech_enhance'
ATTR_IS_COORDINATOR = 'is_coordinator' ATTR_IS_COORDINATOR = 'is_coordinator'
@ -105,6 +108,11 @@ SONOS_UPDATE_ALARM_SCHEMA = SONOS_SCHEMA.extend({
vol.Optional(ATTR_INCLUDE_LINKED_ZONES): cv.boolean, vol.Optional(ATTR_INCLUDE_LINKED_ZONES): cv.boolean,
}) })
SONOS_SET_OPTION_SCHEMA = SONOS_SCHEMA.extend({
vol.Optional(ATTR_NIGHT_SOUND): cv.boolean,
vol.Optional(ATTR_SPEECH_ENHANCE): cv.boolean,
})
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Sonos platform.""" """Set up the Sonos platform."""
@ -140,7 +148,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
hosts = hosts.split(',') if isinstance(hosts, str) else hosts hosts = hosts.split(',') if isinstance(hosts, str) else hosts
players = [] players = []
for host in hosts: for host in hosts:
players.append(soco.SoCo(socket.gethostbyname(host))) try:
players.append(soco.SoCo(socket.gethostbyname(host)))
except OSError:
_LOGGER.warning("Failed to initialize '%s'", host)
if not players: if not players:
players = soco.discover( players = soco.discover(
@ -189,6 +200,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
device.clear_sleep_timer() device.clear_sleep_timer()
elif service.service == SERVICE_UPDATE_ALARM: elif service.service == SERVICE_UPDATE_ALARM:
device.update_alarm(**service.data) device.update_alarm(**service.data)
elif service.service == SERVICE_SET_OPTION:
device.update_option(**service.data)
device.schedule_update_ha_state(True) device.schedule_update_ha_state(True)
@ -221,6 +234,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
descriptions.get(SERVICE_UPDATE_ALARM), descriptions.get(SERVICE_UPDATE_ALARM),
schema=SONOS_UPDATE_ALARM_SCHEMA) schema=SONOS_UPDATE_ALARM_SCHEMA)
hass.services.register(
DOMAIN, SERVICE_SET_OPTION, service_handle,
descriptions.get(SERVICE_SET_OPTION),
schema=SONOS_SET_OPTION_SCHEMA)
def _parse_timespan(timespan): def _parse_timespan(timespan):
"""Parse a time-span into number of seconds.""" """Parse a time-span into number of seconds."""
@ -331,8 +349,11 @@ class SonosDevice(MediaPlayerDevice):
self._support_previous_track = False self._support_previous_track = False
self._support_next_track = False self._support_next_track = False
self._support_play = False self._support_play = False
self._support_shuffle_set = True
self._support_stop = False self._support_stop = False
self._support_pause = False self._support_pause = False
self._night_sound = None
self._speech_enhance = None
self._current_track_uri = None self._current_track_uri = None
self._current_track_is_radio_stream = False self._current_track_is_radio_stream = False
self._queue = None self._queue = None
@ -450,8 +471,11 @@ class SonosDevice(MediaPlayerDevice):
self._support_previous_track = False self._support_previous_track = False
self._support_next_track = False self._support_next_track = False
self._support_play = False self._support_play = False
self._support_shuffle_set = False
self._support_stop = False self._support_stop = False
self._support_pause = False self._support_pause = False
self._night_sound = None
self._speech_enhance = None
self._is_playing_tv = False self._is_playing_tv = False
self._is_playing_line_in = False self._is_playing_line_in = False
self._source_name = None self._source_name = None
@ -524,6 +548,9 @@ class SonosDevice(MediaPlayerDevice):
media_position_updated_at = None media_position_updated_at = None
source_name = None source_name = None
night_sound = self._player.night_mode
speech_enhance = self._player.dialog_mode
is_radio_stream = \ is_radio_stream = \
current_media_uri.startswith('x-sonosapi-stream:') or \ current_media_uri.startswith('x-sonosapi-stream:') or \
current_media_uri.startswith('x-rincon-mp3radio:') current_media_uri.startswith('x-rincon-mp3radio:')
@ -536,6 +563,7 @@ class SonosDevice(MediaPlayerDevice):
support_play = False support_play = False
support_stop = True support_stop = True
support_pause = False support_pause = False
support_shuffle_set = False
if is_playing_tv: if is_playing_tv:
media_artist = SUPPORT_SOURCE_TV media_artist = SUPPORT_SOURCE_TV
@ -558,6 +586,7 @@ class SonosDevice(MediaPlayerDevice):
support_play = True support_play = True
support_stop = True support_stop = True
support_pause = False support_pause = False
support_shuffle_set = False
source_name = 'Radio' source_name = 'Radio'
# Check if currently playing radio station is in favorites # Check if currently playing radio station is in favorites
@ -622,6 +651,7 @@ class SonosDevice(MediaPlayerDevice):
support_play = True support_play = True
support_stop = True support_stop = True
support_pause = True support_pause = True
support_shuffle_set = True
position_info = self._player.avTransport.GetPositionInfo( position_info = self._player.avTransport.GetPositionInfo(
[('InstanceID', 0), [('InstanceID', 0),
@ -694,8 +724,11 @@ class SonosDevice(MediaPlayerDevice):
self._support_previous_track = support_previous_track self._support_previous_track = support_previous_track
self._support_next_track = support_next_track self._support_next_track = support_next_track
self._support_play = support_play self._support_play = support_play
self._support_shuffle_set = support_shuffle_set
self._support_stop = support_stop self._support_stop = support_stop
self._support_pause = support_pause self._support_pause = support_pause
self._night_sound = night_sound
self._speech_enhance = speech_enhance
self._is_playing_tv = is_playing_tv self._is_playing_tv = is_playing_tv
self._is_playing_line_in = is_playing_line_in self._is_playing_line_in = is_playing_line_in
self._source_name = source_name self._source_name = source_name
@ -762,6 +795,11 @@ class SonosDevice(MediaPlayerDevice):
"""Return true if volume is muted.""" """Return true if volume is muted."""
return self._player_volume_muted return self._player_volume_muted
@property
def shuffle(self):
"""Shuffling state."""
return True if self._player.play_mode == 'SHUFFLE' else False
@property @property
def media_content_id(self): def media_content_id(self):
"""Content ID of current playing media.""" """Content ID of current playing media."""
@ -834,6 +872,16 @@ class SonosDevice(MediaPlayerDevice):
return self._media_title return self._media_title
@property
def night_sound(self):
"""Get status of Night Sound."""
return self._night_sound
@property
def speech_enhance(self):
"""Get status of Speech Enhancement."""
return self._speech_enhance
@property @property
def supported_features(self): def supported_features(self):
"""Flag media player features that are supported.""" """Flag media player features that are supported."""
@ -850,7 +898,8 @@ class SonosDevice(MediaPlayerDevice):
if not self._support_play: if not self._support_play:
supported = supported ^ SUPPORT_PLAY supported = supported ^ SUPPORT_PLAY
if not self._support_shuffle_set:
supported = supported ^ SUPPORT_SHUFFLE_SET
if not self._support_stop: if not self._support_stop:
supported = supported ^ SUPPORT_STOP supported = supported ^ SUPPORT_STOP
@ -874,6 +923,11 @@ class SonosDevice(MediaPlayerDevice):
"""Set volume level, range 0..1.""" """Set volume level, range 0..1."""
self._player.volume = str(int(volume * 100)) self._player.volume = str(int(volume * 100))
@soco_error
def set_shuffle(self, shuffle):
"""Enable/Disable shuffle mode."""
self._player.play_mode = 'SHUFFLE' if shuffle else 'NORMAL'
@soco_error @soco_error
def mute_volume(self, mute): def mute_volume(self, mute):
"""Mute (true) or unmute (false) media player.""" """Mute (true) or unmute (false) media player."""
@ -932,7 +986,6 @@ class SonosDevice(MediaPlayerDevice):
self._player.stop() self._player.stop()
self._player.clear_queue() self._player.clear_queue()
self._player.play_mode = 'NORMAL'
self._player.add_to_queue(didl) self._player.add_to_queue(didl)
@property @property
@ -1160,7 +1213,24 @@ class SonosDevice(MediaPlayerDevice):
a.include_linked_zones = data[ATTR_INCLUDE_LINKED_ZONES] a.include_linked_zones = data[ATTR_INCLUDE_LINKED_ZONES]
a.save() a.save()
@soco_error
def update_option(self, **data):
"""Modify playback options."""
if ATTR_NIGHT_SOUND in data and self.night_sound is not None:
self.soco.night_mode = data[ATTR_NIGHT_SOUND]
if ATTR_SPEECH_ENHANCE in data and self.speech_enhance is not None:
self.soco.dialog_mode = data[ATTR_SPEECH_ENHANCE]
@property @property
def device_state_attributes(self): def device_state_attributes(self):
"""Return device specific state attributes.""" """Return device specific state attributes."""
return {ATTR_IS_COORDINATOR: self.is_coordinator} attributes = {ATTR_IS_COORDINATOR: self.is_coordinator}
if self.night_sound is not None:
attributes[ATTR_NIGHT_SOUND] = self.night_sound
if self.speech_enhance is not None:
attributes[ATTR_SPEECH_ENHANCE] = self.speech_enhance
return attributes

View File

@ -0,0 +1,207 @@
"""
Support for Logitech UE Smart Radios.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/media_player.ue_smart_radio/
"""
import logging
import voluptuous as vol
import requests
from homeassistant.components.media_player import (
MediaPlayerDevice, MEDIA_TYPE_MUSIC, PLATFORM_SCHEMA,
SUPPORT_PLAY, SUPPORT_PAUSE, SUPPORT_STOP, SUPPORT_PREVIOUS_TRACK,
SUPPORT_NEXT_TRACK, SUPPORT_TURN_ON, SUPPORT_TURN_OFF, SUPPORT_VOLUME_SET,
SUPPORT_VOLUME_MUTE)
from homeassistant.const import (
CONF_USERNAME, CONF_PASSWORD, STATE_OFF, STATE_IDLE, STATE_PLAYING,
STATE_PAUSED)
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
ICON = "mdi:radio"
URL = "http://decibel.logitechmusic.com/jsonrpc.js"
SUPPORT_UE_SMART_RADIO = SUPPORT_PLAY | SUPPORT_PAUSE | SUPPORT_STOP | \
SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_TURN_ON | \
SUPPORT_TURN_OFF | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE
PLAYBACK_DICT = {"play": STATE_PLAYING,
"pause": STATE_PAUSED,
"stop": STATE_IDLE}
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
})
def send_request(payload, session):
"""Send request to radio."""
try:
request = requests.post(URL,
cookies={"sdi_squeezenetwork_session":
session},
json=payload, timeout=5)
except requests.exceptions.Timeout:
_LOGGER.error("Timed out when sending request")
except requests.exceptions.ConnectionError:
_LOGGER.error("An error occurred while connecting")
else:
return request.json()
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Logitech UE Smart Radio platform."""
email = config.get(CONF_USERNAME)
password = config.get(CONF_PASSWORD)
session_request = requests.post("https://www.uesmartradio.com/user/login",
data={"email": email, "password":
password})
session = session_request.cookies["sdi_squeezenetwork_session"]
player_request = send_request({"params": ["", ["serverstatus"]]}, session)
player_id = player_request["result"]["players_loop"][0]["playerid"]
player_name = player_request["result"]["players_loop"][0]["name"]
add_devices([UERadioDevice(session, player_id, player_name)])
class UERadioDevice(MediaPlayerDevice):
"""Representation of a Logitech UE Smart Radio device."""
def __init__(self, session, player_id, player_name):
"""Initialize the Logitech UE Smart Radio device."""
self._session = session
self._player_id = player_id
self._name = player_name
self._state = None
self._volume = 0
self._last_volume = 0
self._media_title = None
self._media_artist = None
self._media_artwork_url = None
def send_command(self, command):
"""Send command to radio."""
send_request({"method": "slim.request", "params":
[self._player_id, command]}, self._session)
def update(self):
"""Get the latest details from the device."""
request = send_request({
"method": "slim.request", "params":
[self._player_id, ["status", "-", 1,
"tags:cgABbehldiqtyrSuoKLN"]]}, self._session)
if request["error"] is not None:
self._state = None
return
if request["result"]["power"] == 0:
self._state = STATE_OFF
else:
self._state = PLAYBACK_DICT[request["result"]["mode"]]
media_info = request["result"]["playlist_loop"][0]
self._volume = request["result"]["mixer volume"] / 100
self._media_artwork_url = media_info["artwork_url"]
self._media_title = media_info["title"]
if "artist" in media_info:
self._media_artist = media_info["artist"]
else:
self._media_artist = media_info.get("remote_title")
@property
def name(self):
"""Return the name of the device."""
return self._name
@property
def state(self):
"""Return the state of the device."""
return self._state
@property
def icon(self):
"""Return the icon to use in the frontend, if any."""
return ICON
@property
def is_volume_muted(self):
"""Boolean if volume is currently muted."""
return True if self._volume <= 0 else False
@property
def volume_level(self):
"""Volume level of the media player (0..1)."""
return self._volume
@property
def supported_features(self):
"""Flag of features that are supported."""
return SUPPORT_UE_SMART_RADIO
@property
def media_content_type(self):
"""Return the media content type."""
return MEDIA_TYPE_MUSIC
@property
def media_image_url(self):
"""Image URL of current playing media."""
return self._media_artwork_url
@property
def media_artist(self):
"""Artist of current playing media, music track only."""
return self._media_artist
@property
def media_title(self):
"""Title of current playing media."""
return self._media_title
def turn_on(self):
"""Turn on specified media player or all."""
self.send_command(["power", 1])
def turn_off(self):
"""Turn off specified media player or all."""
self.send_command(["power", 0])
def media_play(self):
"""Send the media player the command for play/pause."""
self.send_command(["play"])
def media_pause(self):
"""Send the media player the command for pause."""
self.send_command(["pause"])
def media_stop(self):
"""Send the media player the stop command."""
self.send_command(["stop"])
def media_previous_track(self):
"""Send the media player the command for prev track."""
self.send_command(["button", "rew"])
def media_next_track(self):
"""Send the media player the command for next track."""
self.send_command(["button", "fwd"])
def mute_volume(self, mute):
"""Send mute command."""
if mute:
self._last_volume = self._volume
self.send_command(["mixer", "volume", 0])
else:
self.send_command(["mixer", "volume", self._last_volume * 100])
def set_volume_level(self, volume):
"""Set volume level, range 0..1."""
self.send_command(["mixer", "volume", volume * 100])

View File

@ -322,17 +322,17 @@ class LgWebOSDevice(MediaPlayerDevice):
def select_source(self, source): def select_source(self, source):
"""Select input source.""" """Select input source."""
source = self._source_list.get(source) source_dict = self._source_list.get(source)
if source is None: if source_dict is None:
_LOGGER.warning("Source %s not found for %s", source, self.name) _LOGGER.warning("Source %s not found for %s", source, self.name)
return return
self._current_source_id = self._source_list[source]['id'] self._current_source_id = source_dict['id']
if source.get('title'): if source_dict.get('title'):
self._current_source = self._source_list[source]['title'] self._current_source = source_dict['title']
self._client.launch_app(self._source_list[source]['id']) self._client.launch_app(source_dict['id'])
elif source.get('label'): elif source_dict.get('label'):
self._current_source = self._source_list[source]['label'] self._current_source = source_dict['label']
self._client.set_input(self._source_list[source]['id']) self._client.set_input(source_dict['id'])
def media_play(self): def media_play(self):
"""Send play command.""" """Send play command."""

View File

@ -36,7 +36,7 @@ SUPPORTED_FEATURES = (
KNOWN_HOSTS_KEY = 'data_yamaha_musiccast' KNOWN_HOSTS_KEY = 'data_yamaha_musiccast'
INTERVAL_SECONDS = 'interval_seconds' INTERVAL_SECONDS = 'interval_seconds'
REQUIREMENTS = ['pymusiccast==0.1.5'] REQUIREMENTS = ['pymusiccast==0.1.6']
DEFAULT_PORT = 5005 DEFAULT_PORT = 5005
DEFAULT_INTERVAL = 480 DEFAULT_INTERVAL = 480

View File

@ -5,6 +5,7 @@ For more details about this component, please refer to the documentation at
https://home-assistant.io/components/mochad/ https://home-assistant.io/components/mochad/
""" """
import logging import logging
import threading
import voluptuous as vol import voluptuous as vol
@ -23,6 +24,8 @@ CONF_COMM_TYPE = 'comm_type'
DOMAIN = 'mochad' DOMAIN = 'mochad'
REQ_LOCK = threading.Lock()
CONFIG_SCHEMA = vol.Schema({ CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({ DOMAIN: vol.Schema({
vol.Optional(CONF_HOST, default='localhost'): cv.string, vol.Optional(CONF_HOST, default='localhost'): cv.string,

View File

@ -0,0 +1,60 @@
"""
Support for Vera scenes.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/scene.vera/
"""
import logging
from homeassistant.util import slugify
from homeassistant.components.scene import Scene
from homeassistant.components.vera import (
VERA_CONTROLLER, VERA_SCENES, VERA_ID_FORMAT)
DEPENDENCIES = ['vera']
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Vera scenes."""
add_devices(
[VeraScene(scene, hass.data[VERA_CONTROLLER])
for scene in hass.data[VERA_SCENES]], True)
class VeraScene(Scene):
"""Representation of a Vera scene entity."""
def __init__(self, vera_scene, controller):
"""Initialize the scene."""
self.vera_scene = vera_scene
self.controller = controller
self._name = self.vera_scene.name
# Append device id to prevent name clashes in HA.
self.vera_id = VERA_ID_FORMAT.format(
slugify(vera_scene.name), vera_scene.scene_id)
def update(self):
"""Update the scene status."""
self.vera_scene.refresh()
def activate(self, **kwargs):
"""Activate the scene."""
self.vera_scene.activate()
@property
def name(self):
"""Return the name of the scene."""
return self._name
@property
def device_state_attributes(self):
"""Return the state attributes of the scene."""
return {'vera_scene_id': self.vera_scene.vera_scene_id}
@property
def should_poll(self):
"""Return that polling is not necessary."""
return False

View File

@ -7,25 +7,21 @@ https://home-assistant.io/components/sensor.alarmdecoder/
import asyncio import asyncio
import logging import logging
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.components.alarmdecoder import (SIGNAL_PANEL_MESSAGE) from homeassistant.components.alarmdecoder import (SIGNAL_PANEL_MESSAGE)
from homeassistant.const import (STATE_UNKNOWN)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['alarmdecoder'] DEPENDENCIES = ['alarmdecoder']
@asyncio.coroutine def setup_platform(hass, config, add_devices, discovery_info=None):
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up for AlarmDecoder sensor devices.""" """Set up for AlarmDecoder sensor devices."""
_LOGGER.debug("AlarmDecoderSensor: async_setup_platform") _LOGGER.debug("AlarmDecoderSensor: setup_platform")
device = AlarmDecoderSensor(hass) device = AlarmDecoderSensor(hass)
async_add_devices([device]) add_devices([device])
class AlarmDecoderSensor(Entity): class AlarmDecoderSensor(Entity):
@ -34,23 +30,20 @@ class AlarmDecoderSensor(Entity):
def __init__(self, hass): def __init__(self, hass):
"""Initialize the alarm panel.""" """Initialize the alarm panel."""
self._display = "" self._display = ""
self._state = STATE_UNKNOWN self._state = None
self._icon = 'mdi:alarm-check' self._icon = 'mdi:alarm-check'
self._name = 'Alarm Panel Display' self._name = 'Alarm Panel Display'
_LOGGER.debug("Setting up panel")
@asyncio.coroutine @asyncio.coroutine
def async_added_to_hass(self): def async_added_to_hass(self):
"""Register callbacks.""" """Register callbacks."""
async_dispatcher_connect( self.hass.helpers.dispatcher.async_dispatcher_connect(
self.hass, SIGNAL_PANEL_MESSAGE, self._message_callback) SIGNAL_PANEL_MESSAGE, self._message_callback)
@callback
def _message_callback(self, message): def _message_callback(self, message):
if self._display != message.text: if self._display != message.text:
self._display = message.text self._display = message.text
self.async_schedule_update_ha_state() self.schedule_update_ha_state()
@property @property
def icon(self): def icon(self):

View File

@ -0,0 +1,85 @@
"""
Support for Canary sensors.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.canary/
"""
from homeassistant.components.canary import DATA_CANARY
from homeassistant.const import TEMP_FAHRENHEIT, TEMP_CELSIUS
from homeassistant.helpers.entity import Entity
DEPENDENCIES = ['canary']
SENSOR_VALUE_PRECISION = 1
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Canary sensors."""
data = hass.data[DATA_CANARY]
devices = []
from canary.api import SensorType
for location in data.locations:
for device in location.devices:
if device.is_online:
for sensor_type in SensorType:
devices.append(CanarySensor(data, sensor_type, location,
device))
add_devices(devices, True)
class CanarySensor(Entity):
"""Representation of a Canary sensor."""
def __init__(self, data, sensor_type, location, device):
"""Initialize the sensor."""
self._data = data
self._sensor_type = sensor_type
self._device_id = device.device_id
self._is_celsius = location.is_celsius
self._sensor_value = None
sensor_type_name = sensor_type.value.replace("_", " ").title()
self._name = '{} {} {}'.format(location.name,
device.name,
sensor_type_name)
@property
def name(self):
"""Return the name of the Canary sensor."""
return self._name
@property
def state(self):
"""Return the state of the sensor."""
return self._sensor_value
@property
def unique_id(self):
"""Return the unique ID of this sensor."""
return "sensor_canary_{}_{}".format(self._device_id,
self._sensor_type.value)
@property
def unit_of_measurement(self):
"""Return the unit of measurement this sensor expresses itself in."""
from canary.api import SensorType
if self._sensor_type == SensorType.TEMPERATURE:
return TEMP_CELSIUS if self._is_celsius else TEMP_FAHRENHEIT
elif self._sensor_type == SensorType.HUMIDITY:
return "%"
elif self._sensor_type == SensorType.AIR_QUALITY:
return ""
return None
def update(self):
"""Get the latest state of the sensor."""
self._data.update()
readings = self._data.get_readings(self._device_id)
value = next((
reading.value for reading in readings
if reading.sensor_type == self._sensor_type), None)
if value is not None:
self._sensor_value = round(float(value), SENSOR_VALUE_PRECISION)

View File

@ -0,0 +1,97 @@
"""
Show the amount of records in a user's Discogs collection.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.discogs/
"""
import asyncio
from datetime import timedelta
import logging
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME, CONF_TOKEN
from homeassistant.helpers.aiohttp_client import SERVER_SOFTWARE
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
REQUIREMENTS = ['discogs_client==2.2.1']
_LOGGER = logging.getLogger(__name__)
ATTR_IDENTITY = 'identity'
CONF_ATTRIBUTION = "Data provided by Discogs"
DEFAULT_NAME = 'Discogs'
ICON = 'mdi:album'
SCAN_INTERVAL = timedelta(hours=2)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_TOKEN): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
})
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up the Discogs sensor."""
import discogs_client
name = config.get(CONF_NAME)
token = config.get(CONF_TOKEN)
try:
discogs = discogs_client.Client(SERVER_SOFTWARE, user_token=token)
identity = discogs.identity()
except discogs_client.exceptions.HTTPError:
_LOGGER.error("API token is not valid")
return
async_add_devices([DiscogsSensor(identity, name)], True)
class DiscogsSensor(Entity):
"""Get a user's number of records in collection."""
def __init__(self, identity, name):
"""Initialize the Discogs sensor."""
self._identity = identity
self._name = name
self._state = None
@property
def name(self):
"""Return the name of the sensor."""
return self._name
@property
def state(self):
"""Return the state of the sensor."""
return self._state
@property
def icon(self):
"""Return the icon to use in the frontend, if any."""
return ICON
@property
def unit_of_measurement(self):
"""Return the unit this state is expressed in."""
return 'records'
@property
def device_state_attributes(self):
"""Return the state attributes of the sensor."""
return {
ATTR_ATTRIBUTION: CONF_ATTRIBUTION,
ATTR_IDENTITY: self._identity.name,
}
@asyncio.coroutine
def async_update(self):
"""Set state to the amount of records in user's collection."""
self._state = self._identity.num_collection

View File

@ -31,6 +31,7 @@ CONF_COST = 'cost'
CONF_CURRENT_VALUES = 'current_values' CONF_CURRENT_VALUES = 'current_values'
DEFAULT_PERIOD = 'year' DEFAULT_PERIOD = 'year'
DEFAULT_UTC_OFFSET = '0'
SENSOR_TYPES = { SENSOR_TYPES = {
CONF_INSTANT: ['Energy Usage', 'W'], CONF_INSTANT: ['Energy Usage', 'W'],
@ -50,7 +51,7 @@ SENSORS_SCHEMA = vol.Schema({
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_APPTOKEN): cv.string, vol.Required(CONF_APPTOKEN): cv.string,
vol.Optional(CONF_UTC_OFFSET): cv.string, vol.Optional(CONF_UTC_OFFSET, default=DEFAULT_UTC_OFFSET): cv.string,
vol.Required(CONF_MONITORED_VARIABLES): [SENSORS_SCHEMA] vol.Required(CONF_MONITORED_VARIABLES): [SENSORS_SCHEMA]
}) })

View File

@ -7,10 +7,10 @@ https://www.hydroquebec.com/portail/en/group/clientele/portrait-de-consommation
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.hydroquebec/ https://home-assistant.io/components/sensor.hydroquebec/
""" """
import asyncio
import logging import logging
from datetime import timedelta from datetime import timedelta
import requests
import voluptuous as vol import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.components.sensor import PLATFORM_SCHEMA
@ -21,7 +21,7 @@ from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle from homeassistant.util import Throttle
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['pyhydroquebec==1.3.1'] REQUIREMENTS = ['pyhydroquebec==2.0.2']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -93,7 +93,8 @@ DAILY_MAP = (('yesterday_total_consumption', 'consoTotalQuot'),
('yesterday_higher_price_consumption', 'consoHautQuot')) ('yesterday_higher_price_consumption', 'consoHautQuot'))
def setup_platform(hass, config, add_devices, discovery_info=None): @asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up the HydroQuebec sensor.""" """Set up the HydroQuebec sensor."""
# Create a data fetcher to support all of the configured sensors. Then make # Create a data fetcher to support all of the configured sensors. Then make
# the first call to init the data. # the first call to init the data.
@ -102,13 +103,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
password = config.get(CONF_PASSWORD) password = config.get(CONF_PASSWORD)
contract = config.get(CONF_CONTRACT) contract = config.get(CONF_CONTRACT)
try: hydroquebec_data = HydroquebecData(username, password, contract)
hydroquebec_data = HydroquebecData(username, password, contract) contracts = yield from hydroquebec_data.get_contract_list()
_LOGGER.info("Contract list: %s", _LOGGER.info("Contract list: %s",
", ".join(hydroquebec_data.get_contract_list())) ", ".join(contracts))
except requests.exceptions.HTTPError as error:
_LOGGER.error("Failt login: %s", error)
return False
name = config.get(CONF_NAME) name = config.get(CONF_NAME)
@ -116,7 +114,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
for variable in config[CONF_MONITORED_VARIABLES]: for variable in config[CONF_MONITORED_VARIABLES]:
sensors.append(HydroQuebecSensor(hydroquebec_data, variable, name)) sensors.append(HydroQuebecSensor(hydroquebec_data, variable, name))
add_devices(sensors) async_add_devices(sensors, True)
class HydroQuebecSensor(Entity): class HydroQuebecSensor(Entity):
@ -152,10 +150,11 @@ class HydroQuebecSensor(Entity):
"""Icon to use in the frontend, if any.""" """Icon to use in the frontend, if any."""
return self._icon return self._icon
def update(self): @asyncio.coroutine
def async_update(self):
"""Get the latest data from Hydroquebec and update the state.""" """Get the latest data from Hydroquebec and update the state."""
self.hydroquebec_data.update() yield from self.hydroquebec_data.async_update()
if self.type in self.hydroquebec_data.data: if self.hydroquebec_data.data.get(self.type) is not None:
self._state = round(self.hydroquebec_data.data[self.type], 2) self._state = round(self.hydroquebec_data.data[self.type], 2)
@ -170,23 +169,24 @@ class HydroquebecData(object):
self._contract = contract self._contract = contract
self.data = {} self.data = {}
@asyncio.coroutine
def get_contract_list(self): def get_contract_list(self):
"""Return the contract list.""" """Return the contract list."""
# Fetch data # Fetch data
self._fetch_data() yield from self._fetch_data()
return self.client.get_contracts() return self.client.get_contracts()
@asyncio.coroutine
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def _fetch_data(self): def _fetch_data(self):
"""Fetch latest data from HydroQuebec.""" """Fetch latest data from HydroQuebec."""
from pyhydroquebec.client import PyHydroQuebecError
try: try:
self.client.fetch_data() yield from self.client.fetch_data()
except PyHydroQuebecError as exp: except BaseException as exp:
_LOGGER.error("Error on receive last Hydroquebec data: %s", exp) _LOGGER.error("Error on receive last Hydroquebec data: %s", exp)
return
@Throttle(MIN_TIME_BETWEEN_UPDATES) @asyncio.coroutine
def update(self): def async_update(self):
"""Return the latest collected data from HydroQuebec.""" """Return the latest collected data from HydroQuebec."""
self._fetch_data() yield from self._fetch_data()
self.data = self.client.get_data(self._contract)[self._contract] self.data = self.client.get_data(self._contract)[self._contract]

View File

@ -282,6 +282,9 @@ class ISYSensorDevice(isy.ISYDevice):
@property @property
def state(self) -> str: def state(self) -> str:
"""Get the state of the ISY994 sensor device.""" """Get the state of the ISY994 sensor device."""
if self.is_unknown():
return None
if len(self._node.uom) == 1: if len(self._node.uom) == 1:
if self._node.uom[0] in UOM_TO_STATES: if self._node.uom[0] in UOM_TO_STATES:
states = UOM_TO_STATES.get(self._node.uom[0]) states = UOM_TO_STATES.get(self._node.uom[0])

View File

@ -5,85 +5,94 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.luftdaten/ https://home-assistant.io/components/sensor.luftdaten/
""" """
import asyncio import asyncio
import json
import logging
from datetime import timedelta from datetime import timedelta
import logging
import requests
import voluptuous as vol import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import ( from homeassistant.const import (
CONF_NAME, CONF_RESOURCE, CONF_VERIFY_SSL, CONF_MONITORED_CONDITIONS, ATTR_ATTRIBUTION, CONF_MONITORED_CONDITIONS, CONF_NAME, TEMP_CELSIUS)
TEMP_CELSIUS) from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.entity import Entity
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
REQUIREMENTS = ['luftdaten==0.1.1']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
ATTR_SENSOR_ID = 'sensor_id'
CONF_ATTRIBUTION = "Data provided by luftdaten.info"
VOLUME_MICROGRAMS_PER_CUBIC_METER = 'µg/m3' VOLUME_MICROGRAMS_PER_CUBIC_METER = 'µg/m3'
SENSOR_TEMPERATURE = 'temperature' SENSOR_TEMPERATURE = 'temperature'
SENSOR_HUMIDITY = 'humidity' SENSOR_HUMIDITY = 'humidity'
SENSOR_PM10 = 'P1' SENSOR_PM10 = 'P1'
SENSOR_PM2_5 = 'P2' SENSOR_PM2_5 = 'P2'
SENSOR_PRESSURE = 'pressure'
SENSOR_TYPES = { SENSOR_TYPES = {
SENSOR_TEMPERATURE: ['Temperature', TEMP_CELSIUS], SENSOR_TEMPERATURE: ['Temperature', TEMP_CELSIUS],
SENSOR_HUMIDITY: ['Humidity', '%'], SENSOR_HUMIDITY: ['Humidity', '%'],
SENSOR_PRESSURE: ['Pressure', 'Pa'],
SENSOR_PM10: ['PM10', VOLUME_MICROGRAMS_PER_CUBIC_METER], SENSOR_PM10: ['PM10', VOLUME_MICROGRAMS_PER_CUBIC_METER],
SENSOR_PM2_5: ['PM2.5', VOLUME_MICROGRAMS_PER_CUBIC_METER] SENSOR_PM2_5: ['PM2.5', VOLUME_MICROGRAMS_PER_CUBIC_METER]
} }
DEFAULT_NAME = 'Luftdaten Sensor' DEFAULT_NAME = 'Luftdaten'
DEFAULT_RESOURCE = 'https://api.luftdaten.info/v1/sensor/'
DEFAULT_VERIFY_SSL = True
CONF_SENSORID = 'sensorid' CONF_SENSORID = 'sensorid'
SCAN_INTERVAL = timedelta(minutes=3) MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_SENSORID): cv.positive_int, vol.Required(CONF_SENSORID): cv.positive_int,
vol.Required(CONF_MONITORED_CONDITIONS): vol.Required(CONF_MONITORED_CONDITIONS):
vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]),
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_RESOURCE, default=DEFAULT_RESOURCE): cv.string,
vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean
}) })
@asyncio.coroutine @asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None): def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up the Luftdaten sensor.""" """Set up the Luftdaten sensor."""
from luftdaten import Luftdaten
name = config.get(CONF_NAME) name = config.get(CONF_NAME)
sensorid = config.get(CONF_SENSORID) sensor_id = config.get(CONF_SENSORID)
verify_ssl = config.get(CONF_VERIFY_SSL)
resource = '{}{}/'.format(config.get(CONF_RESOURCE), sensorid) session = async_get_clientsession(hass)
luftdaten = LuftdatenData(Luftdaten(sensor_id, hass.loop, session))
rest_client = LuftdatenData(resource, verify_ssl) yield from luftdaten.async_update()
rest_client.update()
if rest_client.data is None: if luftdaten.data is None:
_LOGGER.error("Unable to fetch Luftdaten data") _LOGGER.error("Sensor is not available: %s", sensor_id)
return False return
devices = [] devices = []
for variable in config[CONF_MONITORED_CONDITIONS]: for variable in config[CONF_MONITORED_CONDITIONS]:
devices.append(LuftdatenSensor(rest_client, name, variable)) if luftdaten.data.values[variable] is None:
_LOGGER.warning("It might be that sensor %s is not providing "
"measurements for %s", sensor_id, variable)
devices.append(LuftdatenSensor(luftdaten, name, variable, sensor_id))
async_add_devices(devices, True) async_add_devices(devices)
class LuftdatenSensor(Entity): class LuftdatenSensor(Entity):
"""Implementation of a LuftdatenSensor sensor.""" """Implementation of a Luftdaten sensor."""
def __init__(self, rest_client, name, sensor_type): def __init__(self, luftdaten, name, sensor_type, sensor_id):
"""Initialize the LuftdatenSensor sensor.""" """Initialize the Luftdaten sensor."""
self.rest_client = rest_client self.luftdaten = luftdaten
self._name = name self._name = name
self._state = None self._state = None
self._sensor_id = sensor_id
self.sensor_type = sensor_type self.sensor_type = sensor_type
self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] self._unit_of_measurement = SENSOR_TYPES[sensor_type][1]
@ -95,48 +104,50 @@ class LuftdatenSensor(Entity):
@property @property
def state(self): def state(self):
"""Return the state of the device.""" """Return the state of the device."""
return self._state return self.luftdaten.data.values[self.sensor_type]
@property @property
def unit_of_measurement(self): def unit_of_measurement(self):
"""Return the unit of measurement of this entity, if any.""" """Return the unit of measurement of this entity, if any."""
return self._unit_of_measurement return self._unit_of_measurement
def update(self): @property
"""Get the latest data from REST API and update the state.""" def device_state_attributes(self):
self.rest_client.update() """Return the state attributes."""
value = self.rest_client.data if self.luftdaten.data.meta is None:
return
if value is None: attr = {
self._state = None ATTR_ATTRIBUTION: CONF_ATTRIBUTION,
else: ATTR_SENSOR_ID: self._sensor_id,
parsed_json = json.loads(value) 'lat': self.luftdaten.data.meta['latitude'],
'long': self.luftdaten.data.meta['longitude'],
}
return attr
log_entries_count = len(parsed_json) - 1 @asyncio.coroutine
latest_log_entry = parsed_json[log_entries_count] def async_update(self):
sensordata_values = latest_log_entry['sensordatavalues'] """Get the latest data from luftdaten.info and update the state."""
for sensordata_value in sensordata_values: try:
if sensordata_value['value_type'] == self.sensor_type: yield from self.luftdaten.async_update()
self._state = sensordata_value['value'] except TypeError:
pass
class LuftdatenData(object): class LuftdatenData(object):
"""Class for handling the data retrieval.""" """Class for handling the data retrieval."""
def __init__(self, resource, verify_ssl): def __init__(self, data):
"""Initialize the data object.""" """Initialize the data object."""
self._request = requests.Request('GET', resource).prepare() self.data = data
self._verify_ssl = verify_ssl
self.data = None @Throttle(MIN_TIME_BETWEEN_UPDATES)
@asyncio.coroutine
def async_update(self):
"""Get the latest data from luftdaten.info."""
from luftdaten.exceptions import LuftdatenError
def update(self):
"""Get the latest data from Luftdaten service."""
try: try:
with requests.Session() as sess: yield from self.data.async_get_data()
response = sess.send( except LuftdatenError:
self._request, timeout=10, verify=self._verify_ssl) _LOGGER.error("Unable to retrieve data from luftdaten.info")
self.data = response.text
except requests.exceptions.RequestException:
_LOGGER.error("Error fetching data: %s", self._request)
self.data = None

View File

@ -12,15 +12,15 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.const import ( from homeassistant.const import (
CONF_MONITORED_CONDITIONS, CONF_NAME, CONF_MAC) CONF_FORCE_UPDATE, CONF_MONITORED_CONDITIONS, CONF_NAME, CONF_MAC
)
REQUIREMENTS = ['miflora==0.1.16'] REQUIREMENTS = ['miflora==0.2.0']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONF_ADAPTER = 'adapter' CONF_ADAPTER = 'adapter'
CONF_CACHE = 'cache_value' CONF_CACHE = 'cache_value'
CONF_FORCE_UPDATE = 'force_update'
CONF_MEDIAN = 'median' CONF_MEDIAN = 'median'
CONF_RETRIES = 'retries' CONF_RETRIES = 'retries'
CONF_TIMEOUT = 'timeout' CONF_TIMEOUT = 'timeout'
@ -60,11 +60,12 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the MiFlora sensor.""" """Set up the MiFlora sensor."""
from miflora import miflora_poller from miflora import miflora_poller
from miflora.backends.gatttool import GatttoolBackend
cache = config.get(CONF_CACHE) cache = config.get(CONF_CACHE)
poller = miflora_poller.MiFloraPoller( poller = miflora_poller.MiFloraPoller(
config.get(CONF_MAC), cache_timeout=cache, config.get(CONF_MAC), cache_timeout=cache,
adapter=config.get(CONF_ADAPTER)) adapter=config.get(CONF_ADAPTER), backend=GatttoolBackend)
force_update = config.get(CONF_FORCE_UPDATE) force_update = config.get(CONF_FORCE_UPDATE)
median = config.get(CONF_MEDIAN) median = config.get(CONF_MEDIAN)
poller.ble_timeout = config.get(CONF_TIMEOUT) poller.ble_timeout = config.get(CONF_TIMEOUT)

View File

@ -13,7 +13,8 @@ import voluptuous as vol
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.components.mqtt import CONF_STATE_TOPIC, CONF_QOS from homeassistant.components.mqtt import CONF_STATE_TOPIC, CONF_QOS
from homeassistant.const import ( from homeassistant.const import (
CONF_NAME, CONF_VALUE_TEMPLATE, STATE_UNKNOWN, CONF_UNIT_OF_MEASUREMENT) CONF_FORCE_UPDATE, CONF_NAME, CONF_VALUE_TEMPLATE, STATE_UNKNOWN,
CONF_UNIT_OF_MEASUREMENT)
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
import homeassistant.components.mqtt as mqtt import homeassistant.components.mqtt as mqtt
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
@ -22,7 +23,6 @@ from homeassistant.util import dt as dt_util
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONF_FORCE_UPDATE = 'force_update'
CONF_EXPIRE_AFTER = 'expire_after' CONF_EXPIRE_AFTER = 'expire_after'
DEFAULT_NAME = 'MQTT Sensor' DEFAULT_NAME = 'MQTT Sensor'

View File

@ -45,7 +45,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
name = config.get(CONF_NAME) name = config.get(CONF_NAME)
monitored_conditions = config.get(CONF_MONITORED_CONDITIONS) monitored_conditions = config.get(CONF_MONITORED_CONDITIONS)
tools = octoprint_api.get_tools() tools = octoprint_api.get_tools()
_LOGGER.error(str(tools))
if "Temperatures" in monitored_conditions: if "Temperatures" in monitored_conditions:
if not tools: if not tools:

View File

@ -10,7 +10,8 @@ import voluptuous as vol
from homeassistant.components.switch import PLATFORM_SCHEMA from homeassistant.components.switch import PLATFORM_SCHEMA
from homeassistant.const import ( from homeassistant.const import (
CONF_NAME, CONF_USERNAME, CONF_PASSWORD, CONF_HOST, CONF_PORT, CONF_TOKEN) CONF_NAME, CONF_USERNAME, CONF_PASSWORD, CONF_HOST, CONF_PORT, CONF_TOKEN,
CONF_SSL)
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle from homeassistant.util import Throttle
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
@ -24,6 +25,7 @@ CONF_SERVER = 'server'
DEFAULT_HOST = 'localhost' DEFAULT_HOST = 'localhost'
DEFAULT_NAME = 'Plex' DEFAULT_NAME = 'Plex'
DEFAULT_PORT = 32400 DEFAULT_PORT = 32400
DEFAULT_SSL = False
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=1) MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=1)
@ -35,6 +37,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_SERVER): cv.string, vol.Optional(CONF_SERVER): cv.string,
vol.Optional(CONF_USERNAME): cv.string, vol.Optional(CONF_USERNAME): cv.string,
vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean,
}) })
@ -48,11 +51,20 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
plex_host = config.get(CONF_HOST) plex_host = config.get(CONF_HOST)
plex_port = config.get(CONF_PORT) plex_port = config.get(CONF_PORT)
plex_token = config.get(CONF_TOKEN) plex_token = config.get(CONF_TOKEN)
plex_url = 'http://{}:{}'.format(plex_host, plex_port)
add_devices([PlexSensor( plex_url = '{}://{}:{}'.format('https' if config.get(CONF_SSL) else 'http',
name, plex_url, plex_user, plex_password, plex_server, plex_host, plex_port)
plex_token)], True)
import plexapi.exceptions
try:
add_devices([PlexSensor(
name, plex_url, plex_user, plex_password, plex_server,
plex_token)], True)
except (plexapi.exceptions.BadRequest, plexapi.exceptions.Unauthorized,
plexapi.exceptions.NotFound) as error:
_LOGGER.error(error)
return
class PlexSensor(Entity): class PlexSensor(Entity):

View File

@ -13,10 +13,11 @@ from requests.auth import HTTPBasicAuth, HTTPDigestAuth
from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import ( from homeassistant.const import (
CONF_PAYLOAD, CONF_NAME, CONF_VALUE_TEMPLATE, CONF_METHOD, CONF_RESOURCE, CONF_AUTHENTICATION, CONF_FORCE_UPDATE, CONF_HEADERS, CONF_NAME,
CONF_UNIT_OF_MEASUREMENT, STATE_UNKNOWN, CONF_VERIFY_SSL, CONF_USERNAME, CONF_METHOD, CONF_PASSWORD, CONF_PAYLOAD, CONF_RESOURCE,
CONF_PASSWORD, CONF_AUTHENTICATION, HTTP_BASIC_AUTHENTICATION, CONF_UNIT_OF_MEASUREMENT, CONF_USERNAME,
HTTP_DIGEST_AUTHENTICATION, CONF_HEADERS) CONF_VALUE_TEMPLATE, CONF_VERIFY_SSL,
HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION, STATE_UNKNOWN)
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
@ -25,6 +26,7 @@ _LOGGER = logging.getLogger(__name__)
DEFAULT_METHOD = 'GET' DEFAULT_METHOD = 'GET'
DEFAULT_NAME = 'REST Sensor' DEFAULT_NAME = 'REST Sensor'
DEFAULT_VERIFY_SSL = True DEFAULT_VERIFY_SSL = True
DEFAULT_FORCE_UPDATE = False
CONF_JSON_ATTRS = 'json_attributes' CONF_JSON_ATTRS = 'json_attributes'
METHODS = ['POST', 'GET'] METHODS = ['POST', 'GET']
@ -43,6 +45,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_USERNAME): cv.string, vol.Optional(CONF_USERNAME): cv.string,
vol.Optional(CONF_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean,
vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean,
}) })
@ -59,6 +62,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
unit = config.get(CONF_UNIT_OF_MEASUREMENT) unit = config.get(CONF_UNIT_OF_MEASUREMENT)
value_template = config.get(CONF_VALUE_TEMPLATE) value_template = config.get(CONF_VALUE_TEMPLATE)
json_attrs = config.get(CONF_JSON_ATTRS) json_attrs = config.get(CONF_JSON_ATTRS)
force_update = config.get(CONF_FORCE_UPDATE)
if value_template is not None: if value_template is not None:
value_template.hass = hass value_template.hass = hass
@ -74,14 +78,15 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
rest.update() rest.update()
add_devices([RestSensor( add_devices([RestSensor(
hass, rest, name, unit, value_template, json_attrs)], True) hass, rest, name, unit, value_template, json_attrs, force_update
)], True)
class RestSensor(Entity): class RestSensor(Entity):
"""Implementation of a REST sensor.""" """Implementation of a REST sensor."""
def __init__(self, hass, rest, name, def __init__(self, hass, rest, name, unit_of_measurement,
unit_of_measurement, value_template, json_attrs): value_template, json_attrs, force_update):
"""Initialize the REST sensor.""" """Initialize the REST sensor."""
self._hass = hass self._hass = hass
self.rest = rest self.rest = rest
@ -91,6 +96,7 @@ class RestSensor(Entity):
self._value_template = value_template self._value_template = value_template
self._json_attrs = json_attrs self._json_attrs = json_attrs
self._attributes = None self._attributes = None
self._force_update = force_update
@property @property
def name(self): def name(self):
@ -112,6 +118,11 @@ class RestSensor(Entity):
"""Return the state of the device.""" """Return the state of the device."""
return self._state return self._state
@property
def force_update(self):
"""Force update."""
return self._force_update
def update(self): def update(self):
"""Get the latest data from REST API and update the state.""" """Get the latest data from REST API and update the state."""
self.rest.update() self.rest.update()

View File

@ -13,7 +13,7 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import (CONF_NAME, ATTR_ATTRIBUTION) from homeassistant.const import (CONF_NAME, ATTR_ATTRIBUTION)
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
REQUIREMENTS = ['python-ripple-api==0.0.2'] REQUIREMENTS = ['python-ripple-api==0.0.3']
CONF_ADDRESS = 'address' CONF_ADDRESS = 'address'
CONF_ATTRIBUTION = "Data provided by ripple.com" CONF_ATTRIBUTION = "Data provided by ripple.com"
@ -71,4 +71,6 @@ class RippleSensor(Entity):
def update(self): def update(self):
"""Get the latest state of the sensor.""" """Get the latest state of the sensor."""
from pyripple import get_balance from pyripple import get_balance
self._state = get_balance(self.address) balance = get_balance(self.address)
if balance is not None:
self._state = balance

View File

@ -14,7 +14,7 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_KEY, CONF_NAME from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_KEY, CONF_NAME
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
REQUIREMENTS = ['shodan==1.7.5'] REQUIREMENTS = ['shodan==1.7.7']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -175,15 +175,20 @@ class StatisticsSensor(Entity):
self._purge_old() self._purge_old()
if not self.is_binary: if not self.is_binary:
try: try: # require only one data point
self.mean = round(statistics.mean(self.states), 2) self.mean = round(statistics.mean(self.states), 2)
self.median = round(statistics.median(self.states), 2) self.median = round(statistics.median(self.states), 2)
except statistics.StatisticsError as err:
_LOGGER.error(err)
self.mean = self.median = STATE_UNKNOWN
try: # require at least two data points
self.stdev = round(statistics.stdev(self.states), 2) self.stdev = round(statistics.stdev(self.states), 2)
self.variance = round(statistics.variance(self.states), 2) self.variance = round(statistics.variance(self.states), 2)
except statistics.StatisticsError as err: except statistics.StatisticsError as err:
_LOGGER.error(err) _LOGGER.error(err)
self.mean = self.median = STATE_UNKNOWN
self.stdev = self.variance = STATE_UNKNOWN self.stdev = self.variance = STATE_UNKNOWN
if self.states: if self.states:
self.total = round(sum(self.states), 2) self.total = round(sum(self.states), 2)
self.min = min(self.states) self.min = min(self.states)

View File

@ -21,12 +21,13 @@ CONF_ACCOUNTS = 'accounts'
ICON = 'mdi:steam' ICON = 'mdi:steam'
STATE_ONLINE = 'Online' STATE_OFFLINE = 'offline'
STATE_BUSY = 'Busy' STATE_ONLINE = 'online'
STATE_AWAY = 'Away' STATE_BUSY = 'busy'
STATE_SNOOZE = 'Snooze' STATE_AWAY = 'away'
STATE_TRADE = 'Trade' STATE_SNOOZE = 'snooze'
STATE_PLAY = 'Play' STATE_LOOKING_TO_TRADE = 'looking_to_trade'
STATE_LOOKING_TO_PLAY = 'looking_to_play'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_API_KEY): cv.string, vol.Required(CONF_API_KEY): cv.string,
@ -40,17 +41,23 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Steam platform.""" """Set up the Steam platform."""
import steam as steamod import steam as steamod
steamod.api.key.set(config.get(CONF_API_KEY)) steamod.api.key.set(config.get(CONF_API_KEY))
# Initialize steammods app list before creating sensors
# to benefit from internal caching of the list.
steam_app_list = steamod.apps.app_list()
add_devices( add_devices(
[SteamSensor(account, [SteamSensor(account,
steamod) for account in config.get(CONF_ACCOUNTS)], True) steamod,
steam_app_list)
for account in config.get(CONF_ACCOUNTS)], True)
class SteamSensor(Entity): class SteamSensor(Entity):
"""A class for the Steam account.""" """A class for the Steam account."""
def __init__(self, account, steamod): def __init__(self, account, steamod, steam_app_list):
"""Initialize the sensor.""" """Initialize the sensor."""
self._steamod = steamod self._steamod = steamod
self._steam_app_list = steam_app_list
self._account = account self._account = account
self._profile = None self._profile = None
self._game = self._state = self._name = self._avatar = None self._game = self._state = self._name = self._avatar = None
@ -75,28 +82,39 @@ class SteamSensor(Entity):
"""Update device state.""" """Update device state."""
try: try:
self._profile = self._steamod.user.profile(self._account) self._profile = self._steamod.user.profile(self._account)
if self._profile.current_game[2] is None: self._game = self._get_current_game()
self._game = 'None'
else:
self._game = self._profile.current_game[2]
self._state = { self._state = {
1: STATE_ONLINE, 1: STATE_ONLINE,
2: STATE_BUSY, 2: STATE_BUSY,
3: STATE_AWAY, 3: STATE_AWAY,
4: STATE_SNOOZE, 4: STATE_SNOOZE,
5: STATE_TRADE, 5: STATE_LOOKING_TO_TRADE,
6: STATE_PLAY, 6: STATE_LOOKING_TO_PLAY,
}.get(self._profile.status, 'Offline') }.get(self._profile.status, STATE_OFFLINE)
self._name = self._profile.persona self._name = self._profile.persona
self._avatar = self._profile.avatar_medium self._avatar = self._profile.avatar_medium
except self._steamod.api.HTTPTimeoutError as error: except self._steamod.api.HTTPTimeoutError as error:
_LOGGER.warning(error) _LOGGER.warning(error)
self._game = self._state = self._name = self._avatar = None self._game = self._state = self._name = self._avatar = None
def _get_current_game(self):
game_id = self._profile.current_game[0]
game_extra_info = self._profile.current_game[2]
if game_extra_info:
return game_extra_info
if game_id and game_id in self._steam_app_list:
# The app list always returns a tuple
# with the game id and the game name
return self._steam_app_list[game_id][1]
return None
@property @property
def device_state_attributes(self): def device_state_attributes(self):
"""Return the state attributes.""" """Return the state attributes."""
return {'game': self._game} return {'game': self._game} if self._game else None
@property @property
def entity_picture(self): def entity_picture(self):

View File

@ -16,7 +16,7 @@ from homeassistant.helpers.entity import Entity
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
REQUIREMENTS = ['psutil==5.4.1'] REQUIREMENTS = ['psutil==5.4.2']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -25,8 +25,8 @@ SCAN_INTERVAL = timedelta(seconds=5)
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Vera controller devices.""" """Set up the Vera controller devices."""
add_devices( add_devices(
VeraSensor(device, VERA_CONTROLLER) VeraSensor(device, hass.data[VERA_CONTROLLER])
for device in VERA_DEVICES['sensor']) for device in hass.data[VERA_DEVICES]['sensor'])
class VeraSensor(VeraDevice, Entity): class VeraSensor(VeraDevice, Entity):

View File

@ -6,8 +6,10 @@ https://home-assistant.io/components/sensor.volvooncall/
""" """
import logging import logging
from math import floor
from homeassistant.components.volvooncall import VolvoEntity, RESOURCES from homeassistant.components.volvooncall import (
VolvoEntity, RESOURCES, CONF_SCANDINAVIAN_MILES)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -26,14 +28,37 @@ class VolvoSensor(VolvoEntity):
def state(self): def state(self):
"""Return the state of the sensor.""" """Return the state of the sensor."""
val = getattr(self.vehicle, self._attribute) val = getattr(self.vehicle, self._attribute)
if val is None:
return val
if self._attribute == 'odometer': if self._attribute == 'odometer':
return round(val / 1000) # km val /= 1000 # m -> km
return val
if 'mil' in self.unit_of_measurement:
val /= 10 # km -> mil
if self._attribute == 'average_fuel_consumption':
val /= 10 # L/1000km -> L/100km
if 'mil' in self.unit_of_measurement:
return round(val, 2)
else:
return round(val, 1)
elif self._attribute == 'distance_to_empty':
return int(floor(val))
else:
return int(round(val))
@property @property
def unit_of_measurement(self): def unit_of_measurement(self):
"""Return the unit of measurement.""" """Return the unit of measurement."""
return RESOURCES[self._attribute][3] unit = RESOURCES[self._attribute][3]
if self._state.config[CONF_SCANDINAVIAN_MILES] and 'km' in unit:
if self._attribute == 'average_fuel_consumption':
return 'L/mil'
else:
return unit.replace('km', 'mil')
return unit
@property @property
def icon(self): def icon(self):

View File

@ -32,55 +32,6 @@ foursquare:
description: Vertical accuracy of the user's location, in meters. description: Vertical accuracy of the user's location, in meters.
example: 1 example: 1
homematic:
virtualkey:
description: Press a virtual key from CCU/Homegear or simulate keypress.
fields:
address:
description: Address of homematic device or BidCoS-RF for virtual remote.
example: BidCoS-RF
channel:
description: Channel for calling a keypress.
example: 1
param:
description: Event to send i.e. PRESS_LONG, PRESS_SHORT.
example: PRESS_LONG
proxy:
description: (Optional) for set a hosts value.
example: Hosts name from config
set_var_value:
description: Set the name of a node.
fields:
entity_id:
description: Name(s) of homematic central to set value.
example: 'homematic.ccu2'
name:
description: Name of the variable to set.
example: 'testvariable'
value:
description: New value
example: 1
set_dev_value:
description: Set a device property on RPC XML interface.
fields:
address:
description: Address of homematic device or BidCoS-RF for virtual remote
example: BidCoS-RF
channel:
description: Channel for calling a keypress
example: 1
param:
description: Event to send i.e. PRESS_LONG, PRESS_SHORT
example: PRESS_LONG
proxy:
description: (Optional) for set a hosts value
example: Hosts name from config
value:
description: New value
example: 1
reconnect:
description: Reconnect to all Homematic Hubs.
microsoft_face: microsoft_face:
create_group: create_group:
description: Create a new person group. description: Create a new person group.

View File

@ -15,7 +15,7 @@ DEPENDENCIES = ['mqtt']
CONF_INTENTS = 'intents' CONF_INTENTS = 'intents'
CONF_ACTION = 'action' CONF_ACTION = 'action'
INTENT_TOPIC = 'hermes/nlu/intentParsed' INTENT_TOPIC = 'hermes/intent/#'
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -32,7 +32,8 @@ INTENT_SCHEMA = vol.Schema({
vol.Required('slotName'): str, vol.Required('slotName'): str,
vol.Required('value'): { vol.Required('value'): {
vol.Required('kind'): str, vol.Required('kind'): str,
vol.Required('value'): cv.match_all vol.Optional('value'): cv.match_all,
vol.Optional('rawValue'): cv.match_all
} }
}] }]
}, extra=vol.ALLOW_EXTRA) }, extra=vol.ALLOW_EXTRA)
@ -59,8 +60,12 @@ def async_setup(hass, config):
return return
intent_type = request['intent']['intentName'].split('__')[-1] intent_type = request['intent']['intentName'].split('__')[-1]
slots = {slot['slotName']: {'value': slot['value']['value']} slots = {}
for slot in request.get('slots', [])} for slot in request.get('slots', []):
if 'value' in slot['value']:
slots[slot['slotName']] = {'value': slot['value']['value']}
else:
slots[slot['slotName']] = {'value': slot['rawValue']}
try: try:
yield from intent.async_handle( yield from intent.async_handle(

View File

@ -69,7 +69,10 @@ class ISYSwitchDevice(isy.ISYDevice, SwitchDevice):
@property @property
def state(self) -> str: def state(self) -> str:
"""Get the state of the ISY994 device.""" """Get the state of the ISY994 device."""
return VALUE_TO_STATE.get(bool(self.value), STATE_UNKNOWN) if self.is_unknown():
return None
else:
return VALUE_TO_STATE.get(bool(self.value), STATE_UNKNOWN)
def turn_off(self, **kwargs) -> None: def turn_off(self, **kwargs) -> None:
"""Send the turn on command to the ISY994 switch.""" """Send the turn on command to the ISY994 switch."""

View File

@ -60,18 +60,21 @@ class MochadSwitch(SwitchDevice):
def turn_on(self, **kwargs): def turn_on(self, **kwargs):
"""Turn the switch on.""" """Turn the switch on."""
self._state = True self._state = True
self.device.send_cmd('on') with mochad.REQ_LOCK:
self._controller.read_data() self.device.send_cmd('on')
self._controller.read_data()
def turn_off(self, **kwargs): def turn_off(self, **kwargs):
"""Turn the switch off.""" """Turn the switch off."""
self._state = False self._state = False
self.device.send_cmd('off') with mochad.REQ_LOCK:
self._controller.read_data() self.device.send_cmd('off')
self._controller.read_data()
def _get_device_status(self): def _get_device_status(self):
"""Get the status of the switch from mochad.""" """Get the status of the switch from mochad."""
status = self.device.get_status().rstrip() with mochad.REQ_LOCK:
status = self.device.get_status().rstrip()
return status == 'on' return status == 'on'
@property @property

View File

@ -141,10 +141,17 @@ class ModbusRegisterSwitch(ModbusCoilSwitch):
self._verify_register = ( self._verify_register = (
verify_register if verify_register else self._register) verify_register if verify_register else self._register)
self._register_type = register_type self._register_type = register_type
self._state_on = (
state_on if state_on else self._command_on) if state_on is not None:
self._state_off = ( self._state_on = state_on
state_off if state_off else self._command_off) else:
self._state_on = self._command_on
if state_off is not None:
self._state_off = state_off
else:
self._state_off = self._command_off
self._is_on = None self._is_on = None
def turn_on(self, **kwargs): def turn_on(self, **kwargs):

View File

@ -19,8 +19,8 @@ _LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Vera switches.""" """Set up the Vera switches."""
add_devices( add_devices(
VeraSwitch(device, VERA_CONTROLLER) for VeraSwitch(device, hass.data[VERA_CONTROLLER]) for
device in VERA_DEVICES['switch']) device in hass.data[VERA_DEVICES]['switch'])
class VeraSwitch(VeraDevice, SwitchDevice): class VeraSwitch(VeraDevice, SwitchDevice):

View File

@ -25,7 +25,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
}) })
REQUIREMENTS = ['python-miio==0.3.2'] REQUIREMENTS = ['python-miio==0.3.3']
ATTR_POWER = 'power' ATTR_POWER = 'power'
ATTR_TEMPERATURE = 'temperature' ATTR_TEMPERATURE = 'temperature'

View File

@ -17,7 +17,7 @@ from homeassistant.const import (
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['tellcore-py==1.1.2', 'tellcore-net==0.3'] REQUIREMENTS = ['tellcore-py==1.1.2', 'tellcore-net==0.4']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -67,7 +67,7 @@ def _discover(hass, config, component_name, found_tellcore_devices):
def setup(hass, config): def setup(hass, config):
"""Set up the Tellstick component.""" """Set up the Tellstick component."""
from tellcore.constants import TELLSTICK_DIM from tellcore.constants import (TELLSTICK_DIM, TELLSTICK_UP)
from tellcore.telldus import AsyncioCallbackDispatcher from tellcore.telldus import AsyncioCallbackDispatcher
from tellcore.telldus import TelldusCore from tellcore.telldus import TelldusCore
from tellcorenet import TellCoreClient from tellcorenet import TellCoreClient
@ -102,16 +102,22 @@ def setup(hass, config):
hass.data[DATA_TELLSTICK] = {device.id: device for hass.data[DATA_TELLSTICK] = {device.id: device for
device in tellcore_devices} device in tellcore_devices}
# Discover the switches
_discover(hass, config, 'switch',
[device.id for device in tellcore_devices
if not device.methods(TELLSTICK_DIM)])
# Discover the lights # Discover the lights
_discover(hass, config, 'light', _discover(hass, config, 'light',
[device.id for device in tellcore_devices [device.id for device in tellcore_devices
if device.methods(TELLSTICK_DIM)]) if device.methods(TELLSTICK_DIM)])
# Discover the cover
_discover(hass, config, 'cover',
[device.id for device in tellcore_devices
if device.methods(TELLSTICK_UP)])
# Discover the switches
_discover(hass, config, 'switch',
[device.id for device in tellcore_devices
if (not device.methods(TELLSTICK_UP) and
not device.methods(TELLSTICK_DIM))])
@callback @callback
def async_handle_callback(tellcore_id, tellcore_command, def async_handle_callback(tellcore_id, tellcore_command,
tellcore_data, cid): tellcore_data, cid):

View File

@ -21,7 +21,7 @@ from homeassistant.const import (
ATTR_ENTITY_ID, CONF_HOST, CONF_NAME, CONF_TOKEN, STATE_OFF, STATE_ON) ATTR_ENTITY_ID, CONF_HOST, CONF_NAME, CONF_TOKEN, STATE_OFF, STATE_ON)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['python-miio==0.3.2'] REQUIREMENTS = ['python-miio==0.3.3']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -19,13 +19,13 @@ from homeassistant.const import (
EVENT_HOMEASSISTANT_STOP, CONF_LIGHTS, CONF_EXCLUDE) EVENT_HOMEASSISTANT_STOP, CONF_LIGHTS, CONF_EXCLUDE)
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
REQUIREMENTS = ['pyvera==0.2.38'] REQUIREMENTS = ['pyvera==0.2.39']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DOMAIN = 'vera' DOMAIN = 'vera'
VERA_CONTROLLER = None VERA_CONTROLLER = 'vera_controller'
CONF_CONTROLLER = 'vera_controller_url' CONF_CONTROLLER = 'vera_controller_url'
@ -34,7 +34,8 @@ VERA_ID_FORMAT = '{}_{}'
ATTR_CURRENT_POWER_W = "current_power_w" ATTR_CURRENT_POWER_W = "current_power_w"
ATTR_CURRENT_ENERGY_KWH = "current_energy_kwh" ATTR_CURRENT_ENERGY_KWH = "current_energy_kwh"
VERA_DEVICES = defaultdict(list) VERA_DEVICES = 'vera_devices'
VERA_SCENES = 'vera_scenes'
VERA_ID_LIST_SCHEMA = vol.Schema([int]) VERA_ID_LIST_SCHEMA = vol.Schema([int])
@ -47,20 +48,20 @@ CONFIG_SCHEMA = vol.Schema({
}, extra=vol.ALLOW_EXTRA) }, extra=vol.ALLOW_EXTRA)
VERA_COMPONENTS = [ VERA_COMPONENTS = [
'binary_sensor', 'sensor', 'light', 'switch', 'lock', 'climate', 'cover' 'binary_sensor', 'sensor', 'light', 'switch',
'lock', 'climate', 'cover', 'scene'
] ]
# pylint: disable=unused-argument, too-many-function-args # pylint: disable=unused-argument, too-many-function-args
def setup(hass, base_config): def setup(hass, base_config):
"""Set up for Vera devices.""" """Set up for Vera devices."""
global VERA_CONTROLLER
import pyvera as veraApi import pyvera as veraApi
def stop_subscription(event): def stop_subscription(event):
"""Shutdown Vera subscriptions and subscription thread on exit.""" """Shutdown Vera subscriptions and subscription thread on exit."""
_LOGGER.info("Shutting down subscriptions") _LOGGER.info("Shutting down subscriptions")
VERA_CONTROLLER.stop() hass.data[VERA_CONTROLLER].stop()
config = base_config.get(DOMAIN) config = base_config.get(DOMAIN)
@ -70,11 +71,14 @@ def setup(hass, base_config):
exclude_ids = config.get(CONF_EXCLUDE) exclude_ids = config.get(CONF_EXCLUDE)
# Initialize the Vera controller. # Initialize the Vera controller.
VERA_CONTROLLER, _ = veraApi.init_controller(base_url) controller, _ = veraApi.init_controller(base_url)
hass.data[VERA_CONTROLLER] = controller
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_subscription) hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_subscription)
try: try:
all_devices = VERA_CONTROLLER.get_devices() all_devices = controller.get_devices()
all_scenes = controller.get_scenes()
except RequestException: except RequestException:
# There was a network related error connecting to the Vera controller. # There was a network related error connecting to the Vera controller.
_LOGGER.exception("Error communicating with Vera API") _LOGGER.exception("Error communicating with Vera API")
@ -84,12 +88,19 @@ def setup(hass, base_config):
devices = [device for device in all_devices devices = [device for device in all_devices
if device.device_id not in exclude_ids] if device.device_id not in exclude_ids]
vera_devices = defaultdict(list)
for device in devices: for device in devices:
device_type = map_vera_device(device, light_ids) device_type = map_vera_device(device, light_ids)
if device_type is None: if device_type is None:
continue continue
VERA_DEVICES[device_type].append(device) vera_devices[device_type].append(device)
hass.data[VERA_DEVICES] = vera_devices
vera_scenes = []
for scene in all_scenes:
vera_scenes.append(scene)
hass.data[VERA_SCENES] = vera_scenes
for component in VERA_COMPONENTS: for component in VERA_COMPONENTS:
discovery.load_platform(hass, component, DOMAIN, {}, base_config) discovery.load_platform(hass, component, DOMAIN, {}, base_config)

View File

@ -26,11 +26,13 @@ REQUIREMENTS = ['volvooncall==0.4.0']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONF_UPDATE_INTERVAL = 'update_interval'
MIN_UPDATE_INTERVAL = timedelta(minutes=1) MIN_UPDATE_INTERVAL = timedelta(minutes=1)
DEFAULT_UPDATE_INTERVAL = timedelta(minutes=1) DEFAULT_UPDATE_INTERVAL = timedelta(minutes=1)
CONF_UPDATE_INTERVAL = 'update_interval'
CONF_REGION = 'region' CONF_REGION = 'region'
CONF_SERVICE_URL = 'service_url' CONF_SERVICE_URL = 'service_url'
CONF_SCANDINAVIAN_MILES = 'scandinavian_miles'
SIGNAL_VEHICLE_SEEN = '{}.vehicle_seen'.format(DOMAIN) SIGNAL_VEHICLE_SEEN = '{}.vehicle_seen'.format(DOMAIN)
@ -41,6 +43,8 @@ RESOURCES = {'position': ('device_tracker',),
'fuel_amount': ('sensor', 'Fuel amount', 'mdi:gas-station', 'L'), 'fuel_amount': ('sensor', 'Fuel amount', 'mdi:gas-station', 'L'),
'fuel_amount_level': ( 'fuel_amount_level': (
'sensor', 'Fuel level', 'mdi:water-percent', '%'), 'sensor', 'Fuel level', 'mdi:water-percent', '%'),
'average_fuel_consumption': (
'sensor', 'Fuel consumption', 'mdi:gas-station', 'L/100 km'),
'distance_to_empty': ('sensor', 'Range', 'mdi:ruler', 'km'), 'distance_to_empty': ('sensor', 'Range', 'mdi:ruler', 'km'),
'washer_fluid_level': ('binary_sensor', 'Washer fluid'), 'washer_fluid_level': ('binary_sensor', 'Washer fluid'),
'brake_fluid': ('binary_sensor', 'Brake Fluid'), 'brake_fluid': ('binary_sensor', 'Brake Fluid'),
@ -61,6 +65,7 @@ CONFIG_SCHEMA = vol.Schema({
cv.ensure_list, [vol.In(RESOURCES)]), cv.ensure_list, [vol.In(RESOURCES)]),
vol.Optional(CONF_REGION): cv.string, vol.Optional(CONF_REGION): cv.string,
vol.Optional(CONF_SERVICE_URL): cv.string, vol.Optional(CONF_SERVICE_URL): cv.string,
vol.Optional(CONF_SCANDINAVIAN_MILES, default=False): cv.boolean,
}), }),
}, extra=vol.ALLOW_EXTRA) }, extra=vol.ALLOW_EXTRA)
@ -123,7 +128,8 @@ class VolvoData:
"""Initialize the component state.""" """Initialize the component state."""
self.entities = {} self.entities = {}
self.vehicles = {} self.vehicles = {}
self.names = config[DOMAIN].get(CONF_NAME) self.config = config[DOMAIN]
self.names = self.config.get(CONF_NAME)
def vehicle_name(self, vehicle): def vehicle_name(self, vehicle):
"""Provide a friendly name for a vehicle.""" """Provide a friendly name for a vehicle."""

View File

@ -110,6 +110,9 @@ sensor:
tts: tts:
- platform: google - platform: google
# Cloud
cloud:
group: !include groups.yaml group: !include groups.yaml
automation: !include automations.yaml automation: !include automations.yaml
script: !include scripts.yaml script: !include scripts.yaml

View File

@ -1,7 +1,7 @@
# coding: utf-8 # coding: utf-8
"""Constants used by Home Assistant components.""" """Constants used by Home Assistant components."""
MAJOR_VERSION = 0 MAJOR_VERSION = 0
MINOR_VERSION = 60 MINOR_VERSION = 61
PATCH_VERSION = '0.dev0' PATCH_VERSION = '0.dev0'
__short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION)
__version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION)
@ -75,6 +75,7 @@ CONF_EXCLUDE = 'exclude'
CONF_FILE_PATH = 'file_path' CONF_FILE_PATH = 'file_path'
CONF_FILENAME = 'filename' CONF_FILENAME = 'filename'
CONF_FOR = 'for' CONF_FOR = 'for'
CONF_FORCE_UPDATE = 'force_update'
CONF_FRIENDLY_NAME = 'friendly_name' CONF_FRIENDLY_NAME = 'friendly_name'
CONF_HEADERS = 'headers' CONF_HEADERS = 'headers'
CONF_HOST = 'host' CONF_HOST = 'host'

View File

@ -149,7 +149,7 @@ def async_load_platform(hass, component, platform, discovered=None,
Use `listen_platform` to register a callback for these events. Use `listen_platform` to register a callback for these events.
Warning: Do not yield from this inside a setup method to avoid a dead lock. Warning: Do not yield from this inside a setup method to avoid a dead lock.
Use `hass.loop.async_add_job(async_load_platform(..))` instead. Use `hass.async_add_job(async_load_platform(..))` instead.
This method is a coroutine. This method is a coroutine.
""" """

View File

@ -5,8 +5,8 @@ pip>=8.0.3
jinja2>=2.9.6 jinja2>=2.9.6
voluptuous==0.10.5 voluptuous==0.10.5
typing>=3,<4 typing>=3,<4
aiohttp==2.3.5 aiohttp==2.3.6
yarl==0.15.0 yarl==0.16.0
async_timeout==2.0.0 async_timeout==2.0.0
chardet==3.0.4 chardet==3.0.4
astral==1.4 astral==1.4

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