mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 03:07:37 +00:00
Merge branch 'dev' into nuheat
This commit is contained in:
commit
bdf64ccbbb
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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))
|
||||||
|
92
homeassistant/components/alarm_control_panel/canary.py
Normal file
92
homeassistant/components/alarm_control_panel/canary.py
Normal 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)
|
@ -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."""
|
||||||
|
@ -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:
|
||||||
|
@ -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
|
||||||
|
@ -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__)
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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()
|
||||||
|
@ -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."""
|
||||||
|
@ -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
|
|
||||||
|
@ -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
|
||||||
|
@ -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):
|
||||||
|
230
homeassistant/components/calendar/caldav.py
Normal file
230
homeassistant/components/calendar/caldav.py
Normal 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
|
95
homeassistant/components/camera/canary.py
Normal file
95
homeassistant/components/camera/canary.py
Normal 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
|
117
homeassistant/components/canary.py
Normal file
117
homeassistant/components/canary.py
Normal 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)
|
@ -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)
|
||||||
|
@ -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):
|
||||||
|
@ -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):
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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 = """
|
||||||
|
@ -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
|
||||||
|
@ -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."""
|
||||||
|
65
homeassistant/components/cover/tellstick.py
Executable file
65
homeassistant/components/cover/tellstick.py
Executable 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
|
@ -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):
|
||||||
|
@ -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()
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
))
|
))
|
||||||
|
@ -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(
|
||||||
|
@ -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'),
|
||||||
|
@ -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'
|
||||||
|
@ -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)
|
||||||
|
@ -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'
|
||||||
|
@ -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:
|
68
homeassistant/components/homematic/services.yaml
Normal file
68
homeassistant/components/homematic/services.yaml
Normal 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
|
246
homeassistant/components/hue.py
Normal file
246
homeassistant/components/hue.py
Normal 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.
|
||||||
|
|
||||||
|

|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
@ -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."""
|
||||||
|
@ -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()
|
||||||
|
112
homeassistant/components/light/greenwave.py
Normal file
112
homeassistant/components/light/greenwave.py
Normal 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']
|
@ -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)
|
||||||
|
@ -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}
|
||||||
|
|
||||||

|
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]
|
||||||
|
@ -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):
|
||||||
|
@ -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
|
||||||
|
@ -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):
|
||||||
|
@ -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
|
||||||
|
@ -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):
|
||||||
|
@ -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
|
||||||
|
@ -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."""
|
||||||
|
@ -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
|
||||||
|
@ -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):
|
||||||
|
@ -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):
|
||||||
|
@ -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__)
|
||||||
|
|
||||||
|
@ -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):
|
||||||
|
@ -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:
|
||||||
|
@ -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
|
||||||
|
@ -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."""
|
||||||
|
@ -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.
|
||||||
|
@ -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
|
||||||
|
207
homeassistant/components/media_player/ue_smart_radio.py
Normal file
207
homeassistant/components/media_player/ue_smart_radio.py
Normal 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])
|
@ -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."""
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
|
60
homeassistant/components/scene/vera.py
Normal file
60
homeassistant/components/scene/vera.py
Normal 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
|
@ -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):
|
||||||
|
85
homeassistant/components/sensor/canary.py
Normal file
85
homeassistant/components/sensor/canary.py
Normal 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)
|
97
homeassistant/components/sensor/discogs.py
Normal file
97
homeassistant/components/sensor/discogs.py
Normal 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
|
@ -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]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -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]
|
||||||
|
@ -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])
|
||||||
|
@ -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
|
|
||||||
|
@ -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)
|
||||||
|
@ -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'
|
||||||
|
@ -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:
|
||||||
|
@ -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):
|
||||||
|
@ -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()
|
||||||
|
@ -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
|
||||||
|
@ -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__)
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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):
|
||||||
|
@ -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__)
|
||||||
|
|
||||||
|
@ -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):
|
||||||
|
@ -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):
|
||||||
|
@ -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.
|
||||||
|
@ -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(
|
||||||
|
@ -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."""
|
||||||
|
@ -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
|
||||||
|
@ -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):
|
||||||
|
@ -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):
|
||||||
|
@ -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'
|
||||||
|
@ -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):
|
||||||
|
@ -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__)
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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."""
|
||||||
|
@ -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
|
||||||
|
@ -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'
|
||||||
|
@ -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.
|
||||||
"""
|
"""
|
||||||
|
@ -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
Loading…
x
Reference in New Issue
Block a user