mirror of
https://github.com/home-assistant/core.git
synced 2025-07-24 21:57:51 +00:00
commit
8be2ac70ec
21
.coveragerc
21
.coveragerc
@ -8,6 +8,9 @@ omit =
|
||||
homeassistant/helpers/signal.py
|
||||
|
||||
# omit pieces of code that rely on external devices being present
|
||||
homeassistant/components/alarmdecoder.py
|
||||
homeassistant/components/*/alarmdecoder.py
|
||||
|
||||
homeassistant/components/apcupsd.py
|
||||
homeassistant/components/*/apcupsd.py
|
||||
|
||||
@ -94,6 +97,9 @@ omit =
|
||||
|
||||
homeassistant/components/*/thinkingcleaner.py
|
||||
|
||||
homeassistant/components/tradfri.py
|
||||
homeassistant/components/*/tradfri.py
|
||||
|
||||
homeassistant/components/twilio.py
|
||||
homeassistant/components/notify/twilio_sms.py
|
||||
homeassistant/components/notify/twilio_call.py
|
||||
@ -159,6 +165,7 @@ omit =
|
||||
homeassistant/components/binary_sensor/flic.py
|
||||
homeassistant/components/binary_sensor/hikvision.py
|
||||
homeassistant/components/binary_sensor/iss.py
|
||||
homeassistant/components/binary_sensor/ping.py
|
||||
homeassistant/components/binary_sensor/rest.py
|
||||
homeassistant/components/browser.py
|
||||
homeassistant/components/camera/amcrest.py
|
||||
@ -175,7 +182,6 @@ omit =
|
||||
homeassistant/components/climate/oem.py
|
||||
homeassistant/components/climate/proliphix.py
|
||||
homeassistant/components/climate/radiotherm.py
|
||||
homeassistant/components/config/zwave.py
|
||||
homeassistant/components/cover/garadget.py
|
||||
homeassistant/components/cover/homematic.py
|
||||
homeassistant/components/cover/myq.py
|
||||
@ -225,15 +231,18 @@ omit =
|
||||
homeassistant/components/light/flux_led.py
|
||||
homeassistant/components/light/hue.py
|
||||
homeassistant/components/light/hyperion.py
|
||||
homeassistant/components/light/lifx.py
|
||||
homeassistant/components/light/lifx/*.py
|
||||
homeassistant/components/light/lifx_legacy.py
|
||||
homeassistant/components/light/limitlessled.py
|
||||
homeassistant/components/light/mystrom.py
|
||||
homeassistant/components/light/osramlightify.py
|
||||
homeassistant/components/light/rpi_gpio_pwm.py
|
||||
homeassistant/components/light/piglow.py
|
||||
homeassistant/components/light/tikteck.py
|
||||
homeassistant/components/light/tradfri.py
|
||||
homeassistant/components/light/x10.py
|
||||
homeassistant/components/light/yeelight.py
|
||||
homeassistant/components/light/yeelightsunflower.py
|
||||
homeassistant/components/light/piglow.py
|
||||
homeassistant/components/light/zengge.py
|
||||
homeassistant/components/lirc.py
|
||||
homeassistant/components/lock/nuki.py
|
||||
@ -274,6 +283,7 @@ omit =
|
||||
homeassistant/components/media_player/samsungtv.py
|
||||
homeassistant/components/media_player/snapcast.py
|
||||
homeassistant/components/media_player/sonos.py
|
||||
homeassistant/components/media_player/spotify.py
|
||||
homeassistant/components/media_player/squeezebox.py
|
||||
homeassistant/components/media_player/vlc.py
|
||||
homeassistant/components/media_player/volumio.py
|
||||
@ -315,6 +325,7 @@ omit =
|
||||
homeassistant/components/remote/harmony.py
|
||||
homeassistant/components/remote/itach.py
|
||||
homeassistant/components/scene/hunterdouglas_powerview.py
|
||||
homeassistant/components/scene/lifx_cloud.py
|
||||
homeassistant/components/sensor/amcrest.py
|
||||
homeassistant/components/sensor/arest.py
|
||||
homeassistant/components/sensor/arwn.py
|
||||
@ -375,6 +386,7 @@ omit =
|
||||
homeassistant/components/sensor/onewire.py
|
||||
homeassistant/components/sensor/openevse.py
|
||||
homeassistant/components/sensor/openexchangerates.py
|
||||
homeassistant/components/sensor/opensky.py
|
||||
homeassistant/components/sensor/openweathermap.py
|
||||
homeassistant/components/sensor/pi_hole.py
|
||||
homeassistant/components/sensor/plex.py
|
||||
@ -432,7 +444,7 @@ omit =
|
||||
homeassistant/components/switch/tplink.py
|
||||
homeassistant/components/switch/transmission.py
|
||||
homeassistant/components/switch/wake_on_lan.py
|
||||
homeassistant/components/telegram_webhooks.py
|
||||
homeassistant/components/telegram_bot/*
|
||||
homeassistant/components/thingspeak.py
|
||||
homeassistant/components/tts/amazon_polly.py
|
||||
homeassistant/components/tts/picotts.py
|
||||
@ -442,7 +454,6 @@ omit =
|
||||
homeassistant/components/weather/openweathermap.py
|
||||
homeassistant/components/weather/zamg.py
|
||||
homeassistant/components/zeroconf.py
|
||||
homeassistant/components/zwave/__init__.py
|
||||
homeassistant/components/zwave/util.py
|
||||
|
||||
|
||||
|
@ -8,6 +8,7 @@ MAINTAINER Paulus Schoutsen <Paulus@PaulusSchoutsen.nl>
|
||||
#ENV INSTALL_OPENZWAVE no
|
||||
#ENV INSTALL_LIBCEC no
|
||||
#ENV INSTALL_PHANTOMJS no
|
||||
#ENV INSTALL_COAP_CLIENT no
|
||||
|
||||
VOLUME /config
|
||||
|
||||
@ -21,7 +22,7 @@ RUN virtualization/Docker/setup_docker_prereqs
|
||||
# Install hass component dependencies
|
||||
COPY requirements_all.txt requirements_all.txt
|
||||
RUN pip3 install --no-cache-dir -r requirements_all.txt && \
|
||||
pip3 install --no-cache-dir mysqlclient psycopg2 uvloop
|
||||
pip3 install --no-cache-dir mysqlclient psycopg2 uvloop cchardet
|
||||
|
||||
# Copy source
|
||||
COPY . .
|
||||
|
@ -27,7 +27,8 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ERROR_LOG_FILENAME = 'home-assistant.log'
|
||||
FIRST_INIT_COMPONENT = set((
|
||||
'recorder', 'mqtt', 'mqtt_eventstream', 'logger', 'introduction'))
|
||||
'recorder', 'mqtt', 'mqtt_eventstream', 'logger', 'introduction',
|
||||
'frontend', 'history'))
|
||||
|
||||
|
||||
def from_config_dict(config: Dict[str, Any],
|
||||
|
119
homeassistant/components/alarm_control_panel/alarmdecoder.py
Normal file
119
homeassistant/components/alarm_control_panel/alarmdecoder.py
Normal file
@ -0,0 +1,119 @@
|
||||
"""
|
||||
Support for AlarmDecoder-based alarm control panels (Honeywell/DSC).
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/alarm_control_panel.alarmdecoder/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
import homeassistant.components.alarm_control_panel as alarm
|
||||
|
||||
from homeassistant.components.alarmdecoder import (DATA_AD,
|
||||
SIGNAL_PANEL_MESSAGE)
|
||||
|
||||
from homeassistant.const import (
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED,
|
||||
STATE_UNKNOWN, STATE_ALARM_TRIGGERED)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['alarmdecoder']
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Perform the setup for AlarmDecoder alarm panels."""
|
||||
_LOGGER.debug("AlarmDecoderAlarmPanel: setup")
|
||||
|
||||
device = AlarmDecoderAlarmPanel("Alarm Panel", hass)
|
||||
|
||||
async_add_devices([device])
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class AlarmDecoderAlarmPanel(alarm.AlarmControlPanel):
|
||||
"""Representation of an AlarmDecoder-based alarm panel."""
|
||||
|
||||
def __init__(self, name, hass):
|
||||
"""Initialize the alarm panel."""
|
||||
self._display = ""
|
||||
self._name = name
|
||||
self._state = STATE_UNKNOWN
|
||||
|
||||
_LOGGER.debug("AlarmDecoderAlarm: Setting up panel")
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
async_dispatcher_connect(
|
||||
self.hass, SIGNAL_PANEL_MESSAGE, self._message_callback)
|
||||
|
||||
@callback
|
||||
def _message_callback(self, message):
|
||||
if message.alarm_sounding or message.fire_alarm:
|
||||
if self._state != STATE_ALARM_TRIGGERED:
|
||||
self._state = STATE_ALARM_TRIGGERED
|
||||
self.hass.async_add_job(self.async_update_ha_state())
|
||||
elif message.armed_away:
|
||||
if self._state != STATE_ALARM_ARMED_AWAY:
|
||||
self._state = STATE_ALARM_ARMED_AWAY
|
||||
self.hass.async_add_job(self.async_update_ha_state())
|
||||
elif message.armed_home:
|
||||
if self._state != STATE_ALARM_ARMED_HOME:
|
||||
self._state = STATE_ALARM_ARMED_HOME
|
||||
self.hass.async_add_job(self.async_update_ha_state())
|
||||
else:
|
||||
if self._state != STATE_ALARM_DISARMED:
|
||||
self._state = STATE_ALARM_DISARMED
|
||||
self.hass.async_add_job(self.async_update_ha_state())
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def code_format(self):
|
||||
"""Regex for code format or None if no code is required."""
|
||||
return '^\\d{4,6}$'
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
return self._state
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
_LOGGER.debug("AlarmDecoderAlarm::alarm_disarm: %s", code)
|
||||
if code:
|
||||
_LOGGER.debug("AlarmDecoderAlarm::alarm_disarm: sending %s1",
|
||||
str(code))
|
||||
self.hass.data[DATA_AD].send("{!s}1".format(code))
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
_LOGGER.debug("AlarmDecoderAlarm::alarm_arm_away: %s", code)
|
||||
if code:
|
||||
_LOGGER.debug("AlarmDecoderAlarm::alarm_arm_away: sending %s2",
|
||||
str(code))
|
||||
self.hass.data[DATA_AD].send("{!s}2".format(code))
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_alarm_arm_home(self, code=None):
|
||||
"""Send arm home command."""
|
||||
_LOGGER.debug("AlarmDecoderAlarm::alarm_arm_home: %s", code)
|
||||
if code:
|
||||
_LOGGER.debug("AlarmDecoderAlarm::alarm_arm_home: sending %s3",
|
||||
str(code))
|
||||
self.hass.data[DATA_AD].send("{!s}3".format(code))
|
171
homeassistant/components/alarmdecoder.py
Normal file
171
homeassistant/components/alarmdecoder.py
Normal file
@ -0,0 +1,171 @@
|
||||
"""
|
||||
Support for AlarmDecoder devices.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/alarmdecoder/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||
|
||||
from homeassistant.helpers.discovery import async_load_platform
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
|
||||
REQUIREMENTS = ['alarmdecoder==0.12.1.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = 'alarmdecoder'
|
||||
|
||||
DATA_AD = 'alarmdecoder'
|
||||
|
||||
|
||||
CONF_DEVICE = 'device'
|
||||
CONF_DEVICE_TYPE = 'type'
|
||||
CONF_DEVICE_HOST = 'host'
|
||||
CONF_DEVICE_PORT = 'port'
|
||||
CONF_DEVICE_PATH = 'path'
|
||||
CONF_DEVICE_BAUD = 'baudrate'
|
||||
|
||||
CONF_ZONES = 'zones'
|
||||
CONF_ZONE_NAME = 'name'
|
||||
CONF_ZONE_TYPE = 'type'
|
||||
|
||||
CONF_PANEL_DISPLAY = 'panel_display'
|
||||
|
||||
DEFAULT_DEVICE_TYPE = 'socket'
|
||||
DEFAULT_DEVICE_HOST = 'localhost'
|
||||
DEFAULT_DEVICE_PORT = 10000
|
||||
DEFAULT_DEVICE_PATH = '/dev/ttyUSB0'
|
||||
DEFAULT_DEVICE_BAUD = 115200
|
||||
|
||||
DEFAULT_PANEL_DISPLAY = False
|
||||
|
||||
DEFAULT_ZONE_TYPE = 'opening'
|
||||
|
||||
SIGNAL_PANEL_MESSAGE = 'alarmdecoder.panel_message'
|
||||
SIGNAL_PANEL_ARM_AWAY = 'alarmdecoder.panel_arm_away'
|
||||
SIGNAL_PANEL_ARM_HOME = 'alarmdecoder.panel_arm_home'
|
||||
SIGNAL_PANEL_DISARM = 'alarmdecoder.panel_disarm'
|
||||
|
||||
SIGNAL_ZONE_FAULT = 'alarmdecoder.zone_fault'
|
||||
SIGNAL_ZONE_RESTORE = 'alarmdecoder.zone_restore'
|
||||
|
||||
DEVICE_SOCKET_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_DEVICE_TYPE): 'socket',
|
||||
vol.Optional(CONF_DEVICE_HOST, default=DEFAULT_DEVICE_HOST): cv.string,
|
||||
vol.Optional(CONF_DEVICE_PORT, default=DEFAULT_DEVICE_PORT): cv.port})
|
||||
|
||||
DEVICE_SERIAL_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_DEVICE_TYPE): 'serial',
|
||||
vol.Optional(CONF_DEVICE_PATH, default=DEFAULT_DEVICE_PATH): cv.string,
|
||||
vol.Optional(CONF_DEVICE_BAUD, default=DEFAULT_DEVICE_BAUD): cv.string})
|
||||
|
||||
DEVICE_USB_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_DEVICE_TYPE): 'usb'})
|
||||
|
||||
ZONE_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_ZONE_NAME): cv.string,
|
||||
vol.Optional(CONF_ZONE_TYPE, default=DEFAULT_ZONE_TYPE): cv.string})
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Required(CONF_DEVICE): vol.Any(DEVICE_SOCKET_SCHEMA,
|
||||
DEVICE_SERIAL_SCHEMA,
|
||||
DEVICE_USB_SCHEMA),
|
||||
vol.Optional(CONF_PANEL_DISPLAY,
|
||||
default=DEFAULT_PANEL_DISPLAY): cv.boolean,
|
||||
vol.Optional(CONF_ZONES): {vol.Coerce(int): ZONE_SCHEMA},
|
||||
}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
"""Common setup for AlarmDecoder devices."""
|
||||
from alarmdecoder import AlarmDecoder
|
||||
from alarmdecoder.devices import (SocketDevice, SerialDevice, USBDevice)
|
||||
|
||||
conf = config.get(DOMAIN)
|
||||
|
||||
device = conf.get(CONF_DEVICE)
|
||||
display = conf.get(CONF_PANEL_DISPLAY)
|
||||
zones = conf.get(CONF_ZONES)
|
||||
|
||||
device_type = device.get(CONF_DEVICE_TYPE)
|
||||
host = DEFAULT_DEVICE_HOST
|
||||
port = DEFAULT_DEVICE_PORT
|
||||
path = DEFAULT_DEVICE_PATH
|
||||
baud = DEFAULT_DEVICE_BAUD
|
||||
|
||||
sync_connect = asyncio.Future(loop=hass.loop)
|
||||
|
||||
def handle_open(device):
|
||||
"""Callback for a 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):
|
||||
"""Callback to handle shutdown alarmdecoder."""
|
||||
_LOGGER.debug("Shutting down alarmdecoder.")
|
||||
controller.close()
|
||||
|
||||
@callback
|
||||
def handle_message(sender, message):
|
||||
"""Callback to handle message from alarmdecoder."""
|
||||
async_dispatcher_send(hass, SIGNAL_PANEL_MESSAGE, message)
|
||||
|
||||
def zone_fault_callback(sender, zone):
|
||||
"""Callback to handle zone fault from alarmdecoder."""
|
||||
async_dispatcher_send(hass, SIGNAL_ZONE_FAULT, zone)
|
||||
|
||||
def zone_restore_callback(sender, zone):
|
||||
"""Callback to handle zone restore from alarmdecoder."""
|
||||
async_dispatcher_send(hass, SIGNAL_ZONE_RESTORE, zone)
|
||||
|
||||
controller = False
|
||||
if device_type == 'socket':
|
||||
host = device.get(CONF_DEVICE_HOST)
|
||||
port = device.get(CONF_DEVICE_PORT)
|
||||
controller = AlarmDecoder(SocketDevice(interface=(host, port)))
|
||||
elif device_type == 'serial':
|
||||
path = device.get(CONF_DEVICE_PATH)
|
||||
baud = device.get(CONF_DEVICE_BAUD)
|
||||
controller = AlarmDecoder(SerialDevice(interface=path))
|
||||
elif device_type == 'usb':
|
||||
AlarmDecoder(USBDevice.find())
|
||||
return False
|
||||
|
||||
controller.on_open += handle_open
|
||||
controller.on_message += handle_message
|
||||
controller.on_zone_fault += zone_fault_callback
|
||||
controller.on_zone_restore += zone_restore_callback
|
||||
|
||||
hass.data[DATA_AD] = controller
|
||||
|
||||
controller.open(baud)
|
||||
|
||||
result = yield from sync_connect
|
||||
|
||||
if not result:
|
||||
return False
|
||||
|
||||
hass.async_add_job(async_load_platform(hass, 'alarm_control_panel', DOMAIN,
|
||||
conf, config))
|
||||
|
||||
if zones:
|
||||
hass.async_add_job(async_load_platform(
|
||||
hass, 'binary_sensor', DOMAIN, {CONF_ZONES: zones}, config))
|
||||
|
||||
if display:
|
||||
hass.async_add_job(async_load_platform(hass, 'sensor', DOMAIN,
|
||||
conf, config))
|
||||
|
||||
return True
|
@ -17,9 +17,9 @@ from homeassistant.bootstrap import ERROR_LOG_FILENAME
|
||||
from homeassistant.const import (
|
||||
EVENT_HOMEASSISTANT_STOP, EVENT_TIME_CHANGED,
|
||||
HTTP_BAD_REQUEST, HTTP_CREATED, HTTP_NOT_FOUND,
|
||||
HTTP_UNPROCESSABLE_ENTITY, MATCH_ALL, URL_API, URL_API_COMPONENTS,
|
||||
MATCH_ALL, URL_API, URL_API_COMPONENTS,
|
||||
URL_API_CONFIG, URL_API_DISCOVERY_INFO, URL_API_ERROR_LOG,
|
||||
URL_API_EVENT_FORWARD, URL_API_EVENTS, URL_API_SERVICES,
|
||||
URL_API_EVENTS, URL_API_SERVICES,
|
||||
URL_API_STATES, URL_API_STATES_ENTITY, URL_API_STREAM, URL_API_TEMPLATE,
|
||||
__version__)
|
||||
from homeassistant.exceptions import TemplateError
|
||||
@ -48,7 +48,6 @@ def setup(hass, config):
|
||||
hass.http.register_view(APIEventView)
|
||||
hass.http.register_view(APIServicesView)
|
||||
hass.http.register_view(APIDomainServicesView)
|
||||
hass.http.register_view(APIEventForwardingView)
|
||||
hass.http.register_view(APIComponentsView)
|
||||
hass.http.register_view(APITemplateView)
|
||||
|
||||
@ -319,79 +318,6 @@ class APIDomainServicesView(HomeAssistantView):
|
||||
return self.json(changed_states)
|
||||
|
||||
|
||||
class APIEventForwardingView(HomeAssistantView):
|
||||
"""View to handle EventForwarding requests."""
|
||||
|
||||
url = URL_API_EVENT_FORWARD
|
||||
name = "api:event-forward"
|
||||
event_forwarder = None
|
||||
|
||||
@asyncio.coroutine
|
||||
def post(self, request):
|
||||
"""Setup an event forwarder."""
|
||||
_LOGGER.warning('Event forwarding is deprecated. '
|
||||
'Will be removed by 0.43')
|
||||
hass = request.app['hass']
|
||||
try:
|
||||
data = yield from request.json()
|
||||
except ValueError:
|
||||
return self.json_message("No data received.", HTTP_BAD_REQUEST)
|
||||
|
||||
try:
|
||||
host = data['host']
|
||||
api_password = data['api_password']
|
||||
except KeyError:
|
||||
return self.json_message("No host or api_password received.",
|
||||
HTTP_BAD_REQUEST)
|
||||
|
||||
try:
|
||||
port = int(data['port']) if 'port' in data else None
|
||||
except ValueError:
|
||||
return self.json_message("Invalid value received for port.",
|
||||
HTTP_UNPROCESSABLE_ENTITY)
|
||||
|
||||
api = rem.API(host, api_password, port)
|
||||
|
||||
valid = yield from hass.loop.run_in_executor(
|
||||
None, api.validate_api)
|
||||
if not valid:
|
||||
return self.json_message("Unable to validate API.",
|
||||
HTTP_UNPROCESSABLE_ENTITY)
|
||||
|
||||
if self.event_forwarder is None:
|
||||
self.event_forwarder = rem.EventForwarder(hass)
|
||||
|
||||
self.event_forwarder.async_connect(api)
|
||||
|
||||
return self.json_message("Event forwarding setup.")
|
||||
|
||||
@asyncio.coroutine
|
||||
def delete(self, request):
|
||||
"""Remove event forwarder."""
|
||||
try:
|
||||
data = yield from request.json()
|
||||
except ValueError:
|
||||
return self.json_message("No data received.", HTTP_BAD_REQUEST)
|
||||
|
||||
try:
|
||||
host = data['host']
|
||||
except KeyError:
|
||||
return self.json_message("No host received.", HTTP_BAD_REQUEST)
|
||||
|
||||
try:
|
||||
port = int(data['port']) if 'port' in data else None
|
||||
except ValueError:
|
||||
return self.json_message("Invalid value received for port.",
|
||||
HTTP_UNPROCESSABLE_ENTITY)
|
||||
|
||||
if self.event_forwarder is not None:
|
||||
api = rem.API(host, None, port)
|
||||
|
||||
self.event_forwarder.async_disconnect(api)
|
||||
|
||||
return self.json_message("Event forwarding cancelled.")
|
||||
|
||||
|
||||
class APIComponentsView(HomeAssistantView):
|
||||
"""View to handle Components requests."""
|
||||
|
||||
|
@ -13,7 +13,7 @@ from homeassistant.const import (
|
||||
from homeassistant.const import CONF_PORT
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['PyMata==2.13']
|
||||
REQUIREMENTS = ['PyMata==2.14']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -29,18 +29,25 @@ CONFIG_SCHEMA = vol.Schema({
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Setup the Arduino component."""
|
||||
"""Set up the Arduino component."""
|
||||
import serial
|
||||
|
||||
port = config[DOMAIN][CONF_PORT]
|
||||
|
||||
global BOARD
|
||||
try:
|
||||
BOARD = ArduinoBoard(config[DOMAIN][CONF_PORT])
|
||||
BOARD = ArduinoBoard(port)
|
||||
except (serial.serialutil.SerialException, FileNotFoundError):
|
||||
_LOGGER.exception("Your port is not accessible.")
|
||||
_LOGGER.error("Your port %s is not accessible", port)
|
||||
return False
|
||||
|
||||
if BOARD.get_firmata()[1] <= 2:
|
||||
_LOGGER.error("The StandardFirmata sketch should be 2.2 or newer.")
|
||||
return False
|
||||
try:
|
||||
if BOARD.get_firmata()[1] <= 2:
|
||||
_LOGGER.error("The StandardFirmata sketch should be 2.2 or newer")
|
||||
return False
|
||||
except IndexError:
|
||||
_LOGGER.warning("The version of the StandardFirmata sketch was not"
|
||||
"detected. This may lead to side effects")
|
||||
|
||||
def stop_arduino(event):
|
||||
"""Stop the Arduino service."""
|
||||
@ -67,25 +74,20 @@ class ArduinoBoard(object):
|
||||
def set_mode(self, pin, direction, mode):
|
||||
"""Set the mode and the direction of a given pin."""
|
||||
if mode == 'analog' and direction == 'in':
|
||||
self._board.set_pin_mode(pin,
|
||||
self._board.INPUT,
|
||||
self._board.ANALOG)
|
||||
self._board.set_pin_mode(
|
||||
pin, self._board.INPUT, self._board.ANALOG)
|
||||
elif mode == 'analog' and direction == 'out':
|
||||
self._board.set_pin_mode(pin,
|
||||
self._board.OUTPUT,
|
||||
self._board.ANALOG)
|
||||
self._board.set_pin_mode(
|
||||
pin, self._board.OUTPUT, self._board.ANALOG)
|
||||
elif mode == 'digital' and direction == 'in':
|
||||
self._board.set_pin_mode(pin,
|
||||
self._board.INPUT,
|
||||
self._board.DIGITAL)
|
||||
self._board.set_pin_mode(
|
||||
pin, self._board.INPUT, self._board.DIGITAL)
|
||||
elif mode == 'digital' and direction == 'out':
|
||||
self._board.set_pin_mode(pin,
|
||||
self._board.OUTPUT,
|
||||
self._board.DIGITAL)
|
||||
self._board.set_pin_mode(
|
||||
pin, self._board.OUTPUT, self._board.DIGITAL)
|
||||
elif mode == 'pwm':
|
||||
self._board.set_pin_mode(pin,
|
||||
self._board.OUTPUT,
|
||||
self._board.PWM)
|
||||
self._board.set_pin_mode(
|
||||
pin, self._board.OUTPUT, self._board.PWM)
|
||||
|
||||
def get_analog_inputs(self):
|
||||
"""Get the values from the pins."""
|
||||
|
@ -263,15 +263,23 @@ class AutomationEntity(ToggleEntity):
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self) -> None:
|
||||
"""Startup with initial state or previous state."""
|
||||
enable_automation = DEFAULT_INITIAL_STATE
|
||||
|
||||
if self._initial_state is not None:
|
||||
enable_automation = self._initial_state
|
||||
_LOGGER.debug("Automation %s initial state %s from config "
|
||||
"initial_state", self.entity_id, enable_automation)
|
||||
else:
|
||||
state = yield from async_get_last_state(self.hass, self.entity_id)
|
||||
if state:
|
||||
enable_automation = state.state == STATE_ON
|
||||
self._last_triggered = state.attributes.get('last_triggered')
|
||||
_LOGGER.debug("Automation %s initial state %s from recorder "
|
||||
"last state %s", self.entity_id,
|
||||
enable_automation, state)
|
||||
else:
|
||||
enable_automation = DEFAULT_INITIAL_STATE
|
||||
_LOGGER.debug("Automation %s initial state %s from default "
|
||||
"initial state", self.entity_id,
|
||||
enable_automation)
|
||||
|
||||
if not enable_automation:
|
||||
return
|
||||
|
123
homeassistant/components/binary_sensor/alarmdecoder.py
Normal file
123
homeassistant/components/binary_sensor/alarmdecoder.py
Normal file
@ -0,0 +1,123 @@
|
||||
"""
|
||||
Support for AlarmDecoder zone states- represented as binary sensors.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.alarmdecoder/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
|
||||
from homeassistant.const import (STATE_ON, STATE_OFF, STATE_OPEN, STATE_CLOSED)
|
||||
from homeassistant.components.alarmdecoder import (ZONE_SCHEMA,
|
||||
CONF_ZONES,
|
||||
CONF_ZONE_NAME,
|
||||
CONF_ZONE_TYPE,
|
||||
SIGNAL_ZONE_FAULT,
|
||||
SIGNAL_ZONE_RESTORE)
|
||||
|
||||
|
||||
DEPENDENCIES = ['alarmdecoder']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Setup AlarmDecoder binary sensor devices."""
|
||||
configured_zones = discovery_info[CONF_ZONES]
|
||||
|
||||
devices = []
|
||||
|
||||
for zone_num in configured_zones:
|
||||
device_config_data = ZONE_SCHEMA(configured_zones[zone_num])
|
||||
zone_type = device_config_data[CONF_ZONE_TYPE]
|
||||
zone_name = device_config_data[CONF_ZONE_NAME]
|
||||
device = AlarmDecoderBinarySensor(hass,
|
||||
zone_num,
|
||||
zone_name,
|
||||
zone_type)
|
||||
devices.append(device)
|
||||
|
||||
async_add_devices(devices)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class AlarmDecoderBinarySensor(BinarySensorDevice):
|
||||
"""Representation of an AlarmDecoder binary sensor."""
|
||||
|
||||
def __init__(self, hass, zone_number, zone_name, zone_type):
|
||||
"""Initialize the binary_sensor."""
|
||||
self._zone_number = zone_number
|
||||
self._zone_type = zone_type
|
||||
self._state = 0
|
||||
self._name = zone_name
|
||||
self._type = zone_type
|
||||
|
||||
_LOGGER.debug('AlarmDecoderBinarySensor: Setup up zone: ' + zone_name)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
async_dispatcher_connect(
|
||||
self.hass, SIGNAL_ZONE_FAULT, self._fault_callback)
|
||||
|
||||
async_dispatcher_connect(
|
||||
self.hass, SIGNAL_ZONE_RESTORE, self._restore_callback)
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the binary sensor."""
|
||||
if self._type == 'opening':
|
||||
return STATE_OPEN if self.is_on else STATE_CLOSED
|
||||
|
||||
return STATE_ON if self.is_on else STATE_OFF
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the entity."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Icon for device by its type."""
|
||||
if "window" in self._name.lower():
|
||||
return "mdi:window-open" if self.is_on else "mdi:window-closed"
|
||||
|
||||
if self._type == 'smoke':
|
||||
return "mdi:fire"
|
||||
|
||||
return None
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if sensor is on."""
|
||||
return self._state == 1
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of this sensor, from DEVICE_CLASSES."""
|
||||
return self._zone_type
|
||||
|
||||
@callback
|
||||
def _fault_callback(self, zone):
|
||||
"""Update the zone's state, if needed."""
|
||||
if zone is None or int(zone) == self._zone_number:
|
||||
self._state = 1
|
||||
self.hass.async_add_job(self.async_update_ha_state())
|
||||
|
||||
@callback
|
||||
def _restore_callback(self, zone):
|
||||
"""Update the zone's state, if needed."""
|
||||
if zone is None or int(zone) == self._zone_number:
|
||||
self._state = 0
|
||||
self.hass.async_add_job(self.async_update_ha_state())
|
@ -1,4 +1,9 @@
|
||||
"""Contains functionality to use flic buttons as a binary sensor."""
|
||||
"""
|
||||
Support to use flic buttons as a binary sensor.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.flic/
|
||||
"""
|
||||
import logging
|
||||
import threading
|
||||
|
||||
@ -11,39 +16,40 @@ from homeassistant.const import (
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, PLATFORM_SCHEMA)
|
||||
|
||||
|
||||
REQUIREMENTS = ['https://github.com/soldag/pyflic/archive/0.4.zip#pyflic==0.4']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_TIMEOUT = 3
|
||||
|
||||
CLICK_TYPE_SINGLE = "single"
|
||||
CLICK_TYPE_DOUBLE = "double"
|
||||
CLICK_TYPE_HOLD = "hold"
|
||||
CLICK_TYPE_SINGLE = 'single'
|
||||
CLICK_TYPE_DOUBLE = 'double'
|
||||
CLICK_TYPE_HOLD = 'hold'
|
||||
CLICK_TYPES = [CLICK_TYPE_SINGLE, CLICK_TYPE_DOUBLE, CLICK_TYPE_HOLD]
|
||||
|
||||
CONF_IGNORED_CLICK_TYPES = "ignored_click_types"
|
||||
CONF_IGNORED_CLICK_TYPES = 'ignored_click_types'
|
||||
|
||||
EVENT_NAME = "flic_click"
|
||||
EVENT_DATA_NAME = "button_name"
|
||||
EVENT_DATA_ADDRESS = "button_address"
|
||||
EVENT_DATA_TYPE = "click_type"
|
||||
EVENT_DATA_QUEUED_TIME = "queued_time"
|
||||
DEFAULT_HOST = 'localhost'
|
||||
DEFAULT_PORT = 5551
|
||||
|
||||
EVENT_NAME = 'flic_click'
|
||||
EVENT_DATA_NAME = 'button_name'
|
||||
EVENT_DATA_ADDRESS = 'button_address'
|
||||
EVENT_DATA_TYPE = 'click_type'
|
||||
EVENT_DATA_QUEUED_TIME = 'queued_time'
|
||||
|
||||
# Validation of the user's configuration
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_HOST, default='localhost'): cv.string,
|
||||
vol.Optional(CONF_PORT, default=5551): cv.port,
|
||||
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
vol.Optional(CONF_DISCOVERY, default=True): cv.boolean,
|
||||
vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
|
||||
vol.Optional(CONF_IGNORED_CLICK_TYPES): vol.All(cv.ensure_list,
|
||||
[vol.In(CLICK_TYPES)])
|
||||
vol.Optional(CONF_IGNORED_CLICK_TYPES):
|
||||
vol.All(cv.ensure_list, [vol.In(CLICK_TYPES)])
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Setup the flic platform."""
|
||||
"""Set up the flic platform."""
|
||||
import pyflic
|
||||
|
||||
# Initialize flic client responsible for
|
||||
@ -55,11 +61,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
try:
|
||||
client = pyflic.FlicClient(host, port)
|
||||
except ConnectionRefusedError:
|
||||
_LOGGER.error("Failed to connect to flic server.")
|
||||
_LOGGER.error("Failed to connect to flic server")
|
||||
return
|
||||
|
||||
def new_button_callback(address):
|
||||
"""Setup newly verified button as device in home assistant."""
|
||||
"""Set up newly verified button as device in Home Assistant."""
|
||||
setup_button(hass, config, add_entities, client, address)
|
||||
|
||||
client.on_new_verified_button = new_button_callback
|
||||
@ -74,7 +80,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
|
||||
def get_info_callback(items):
|
||||
"""Add entities for already verified buttons."""
|
||||
addresses = items["bd_addr_of_verified_buttons"] or []
|
||||
addresses = items['bd_addr_of_verified_buttons'] or []
|
||||
for address in addresses:
|
||||
setup_button(hass, config, add_entities, client, address)
|
||||
|
||||
@ -83,7 +89,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
|
||||
|
||||
def start_scanning(config, add_entities, client):
|
||||
"""Start a new flic client for scanning & connceting to new buttons."""
|
||||
"""Start a new flic client for scanning and connecting to new buttons."""
|
||||
import pyflic
|
||||
|
||||
scan_wizard = pyflic.ScanWizard()
|
||||
@ -91,10 +97,10 @@ def start_scanning(config, add_entities, client):
|
||||
def scan_completed_callback(scan_wizard, result, address, name):
|
||||
"""Restart scan wizard to constantly check for new buttons."""
|
||||
if result == pyflic.ScanWizardResult.WizardSuccess:
|
||||
_LOGGER.info("Found new button (%s)", address)
|
||||
_LOGGER.info("Found new button %s", address)
|
||||
elif result != pyflic.ScanWizardResult.WizardFailedTimeout:
|
||||
_LOGGER.warning("Failed to connect to button (%s). Reason: %s",
|
||||
address, result)
|
||||
_LOGGER.warning(
|
||||
"Failed to connect to button %s. Reason: %s", address, result)
|
||||
|
||||
# Restart scan wizard
|
||||
start_scanning(config, add_entities, client)
|
||||
@ -108,7 +114,7 @@ def setup_button(hass, config, add_entities, client, address):
|
||||
timeout = config.get(CONF_TIMEOUT)
|
||||
ignored_click_types = config.get(CONF_IGNORED_CLICK_TYPES)
|
||||
button = FlicButton(hass, client, address, timeout, ignored_click_types)
|
||||
_LOGGER.info("Connected to button (%s)", address)
|
||||
_LOGGER.info("Connected to button %s", address)
|
||||
|
||||
add_entities([button])
|
||||
|
||||
@ -161,7 +167,7 @@ class FlicButton(BinarySensorDevice):
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
return "flic_%s" % self.address.replace(":", "")
|
||||
return 'flic_{}'.format(self.address.replace(':', ''))
|
||||
|
||||
@property
|
||||
def address(self):
|
||||
@ -181,21 +187,21 @@ class FlicButton(BinarySensorDevice):
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return device specific state attributes."""
|
||||
return {"address": self.address}
|
||||
return {'address': self.address}
|
||||
|
||||
def _queued_event_check(self, click_type, time_diff):
|
||||
"""Generate a log message and returns true if timeout exceeded."""
|
||||
time_string = "{:d} {}".format(
|
||||
time_diff, "second" if time_diff == 1 else "seconds")
|
||||
time_diff, 'second' if time_diff == 1 else 'seconds')
|
||||
|
||||
if time_diff > self._timeout:
|
||||
_LOGGER.warning(
|
||||
"Queued %s dropped for %s. Time in queue was %s.",
|
||||
"Queued %s dropped for %s. Time in queue was %s",
|
||||
click_type, self.address, time_string)
|
||||
return True
|
||||
else:
|
||||
_LOGGER.info(
|
||||
"Queued %s allowed for %s. Time in queue was %s.",
|
||||
"Queued %s allowed for %s. Time in queue was %s",
|
||||
click_type, self.address, time_string)
|
||||
return False
|
||||
|
||||
@ -227,8 +233,8 @@ class FlicButton(BinarySensorDevice):
|
||||
EVENT_DATA_TYPE: hass_click_type
|
||||
})
|
||||
|
||||
def _connection_status_changed(self, channel,
|
||||
connection_status, disconnect_reason):
|
||||
def _connection_status_changed(
|
||||
self, channel, connection_status, disconnect_reason):
|
||||
"""Remove device, if button disconnects."""
|
||||
import pyflic
|
||||
|
||||
|
130
homeassistant/components/binary_sensor/ping.py
Normal file
130
homeassistant/components/binary_sensor/ping.py
Normal file
@ -0,0 +1,130 @@
|
||||
"""
|
||||
Tracks the latency of a host by sending ICMP echo requests (ping).
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.ping/
|
||||
"""
|
||||
import logging
|
||||
import subprocess
|
||||
import re
|
||||
import sys
|
||||
from datetime import timedelta
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, PLATFORM_SCHEMA)
|
||||
from homeassistant.const import CONF_NAME, CONF_HOST
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_ROUND_TRIP_TIME_AVG = 'round_trip_time_avg'
|
||||
ATTR_ROUND_TRIP_TIME_MAX = 'round_trip_time_max'
|
||||
ATTR_ROUND_TRIP_TIME_MDEV = 'round_trip_time_mdev'
|
||||
ATTR_ROUND_TRIP_TIME_MIN = 'round_trip_time_min'
|
||||
|
||||
CONF_PING_COUNT = 'count'
|
||||
|
||||
DEFAULT_NAME = 'Ping Binary sensor'
|
||||
DEFAULT_PING_COUNT = 5
|
||||
DEFAULT_SENSOR_CLASS = 'connectivity'
|
||||
|
||||
SCAN_INTERVAL = timedelta(minutes=5)
|
||||
|
||||
PING_MATCHER = re.compile(
|
||||
r'(?P<min>\d+.\d+)\/(?P<avg>\d+.\d+)\/(?P<max>\d+.\d+)\/(?P<mdev>\d+.\d+)')
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_PING_COUNT, default=DEFAULT_PING_COUNT): cv.positive_int,
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Ping Binary sensor."""
|
||||
name = config.get(CONF_NAME)
|
||||
host = config.get(CONF_HOST)
|
||||
count = config.get(CONF_PING_COUNT)
|
||||
|
||||
add_devices([PingBinarySensor(name, PingData(host, count))], True)
|
||||
|
||||
|
||||
class PingBinarySensor(BinarySensorDevice):
|
||||
"""Representation of a Ping Binary sensor."""
|
||||
|
||||
def __init__(self, name, ping):
|
||||
"""Initialize the Ping Binary sensor."""
|
||||
self._name = name
|
||||
self.ping = ping
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of this sensor."""
|
||||
return DEFAULT_SENSOR_CLASS
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if the binary sensor is on."""
|
||||
return self.ping.available
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes of the ICMP checo request."""
|
||||
if self.ping.data is not False:
|
||||
return {
|
||||
ATTR_ROUND_TRIP_TIME_AVG: self.ping.data['avg'],
|
||||
ATTR_ROUND_TRIP_TIME_MAX: self.ping.data['max'],
|
||||
ATTR_ROUND_TRIP_TIME_MDEV: self.ping.data['mdev'],
|
||||
ATTR_ROUND_TRIP_TIME_MIN: self.ping.data['min'],
|
||||
}
|
||||
|
||||
def update(self):
|
||||
"""Get the latest data."""
|
||||
self.ping.update()
|
||||
|
||||
|
||||
class PingData(object):
|
||||
"""The Class for handling the data retrieval."""
|
||||
|
||||
def __init__(self, host, count):
|
||||
"""Initialize the data object."""
|
||||
self._ip_address = host
|
||||
self._count = count
|
||||
self.data = {}
|
||||
self.available = False
|
||||
|
||||
if sys.platform == 'win32':
|
||||
self._ping_cmd = [
|
||||
'ping', '-n', str(self._count), '-w 1000', self._ip_address]
|
||||
else:
|
||||
self._ping_cmd = [
|
||||
'ping', '-n', '-q', '-c', str(self._count), '-W1',
|
||||
self._ip_address]
|
||||
|
||||
def ping(self):
|
||||
"""Send ICMP echo request and return details if success."""
|
||||
pinger = subprocess.Popen(
|
||||
self._ping_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
try:
|
||||
out = pinger.communicate()
|
||||
match = PING_MATCHER.search(str(out).split('\n')[-1])
|
||||
rtt_min, rtt_avg, rtt_max, rtt_mdev = match.groups()
|
||||
return {
|
||||
'min': rtt_min,
|
||||
'avg': rtt_avg,
|
||||
'max': rtt_max,
|
||||
'mdev': rtt_mdev}
|
||||
except (subprocess.CalledProcessError, AttributeError):
|
||||
return False
|
||||
|
||||
def update(self):
|
||||
"""Retrieve the latest details from the host."""
|
||||
self.data = self.ping()
|
||||
self.available = bool(self.data)
|
@ -20,8 +20,8 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
import pywemo.discovery as discovery
|
||||
|
||||
if discovery_info is not None:
|
||||
location = discovery_info[2]
|
||||
mac = discovery_info[3]
|
||||
location = discovery_info['ssdp_description']
|
||||
mac = discovery_info['mac_address']
|
||||
device = discovery.device_from_description(location, mac)
|
||||
|
||||
if device:
|
||||
@ -40,12 +40,14 @@ class WemoBinarySensor(BinarySensorDevice):
|
||||
wemo.SUBSCRIPTION_REGISTRY.register(self.wemo)
|
||||
wemo.SUBSCRIPTION_REGISTRY.on(self.wemo, None, self._update_callback)
|
||||
|
||||
def _update_callback(self, _device, _params):
|
||||
"""Called by the wemo device callback to update state."""
|
||||
def _update_callback(self, _device, _type, _params):
|
||||
"""Called by the Wemo device callback to update state."""
|
||||
_LOGGER.info(
|
||||
'Subscription update for %s',
|
||||
_device)
|
||||
self.update()
|
||||
updated = self.wemo.subscription_update(_type, _params)
|
||||
self._update(force_update=(not updated))
|
||||
|
||||
if not hasattr(self, 'hass'):
|
||||
return
|
||||
self.schedule_update_ha_state()
|
||||
@ -72,7 +74,11 @@ class WemoBinarySensor(BinarySensorDevice):
|
||||
|
||||
def update(self):
|
||||
"""Update WeMo state."""
|
||||
self._update(force_update=True)
|
||||
|
||||
def _update(self, force_update=True):
|
||||
try:
|
||||
self._state = self.wemo.get_state(True)
|
||||
except AttributeError:
|
||||
_LOGGER.warning('Could not update status for %s', self.name)
|
||||
self._state = self.wemo.get_state(force_update)
|
||||
except AttributeError as err:
|
||||
_LOGGER.warning('Could not update status for %s (%s)',
|
||||
self.name, err)
|
||||
|
@ -18,7 +18,7 @@ from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import (
|
||||
async_get_clientsession, async_aiohttp_proxy_web)
|
||||
|
||||
REQUIREMENTS = ['amcrest==1.1.8']
|
||||
REQUIREMENTS = ['amcrest==1.1.9']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
74
homeassistant/components/camera/mqtt.py
Executable file
74
homeassistant/components/camera/mqtt.py
Executable file
@ -0,0 +1,74 @@
|
||||
"""
|
||||
Camera that loads a picture from an MQTT topic.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/camera.mqtt/
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
import homeassistant.components.mqtt as mqtt
|
||||
from homeassistant.const import CONF_NAME
|
||||
from homeassistant.components.camera import Camera, PLATFORM_SCHEMA
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_TOPIC = 'topic'
|
||||
|
||||
DEFAULT_NAME = 'MQTT Camera'
|
||||
|
||||
DEPENDENCIES = ['mqtt']
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_TOPIC): mqtt.valid_subscribe_topic,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string
|
||||
})
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Setup the Camera."""
|
||||
topic = config[CONF_TOPIC]
|
||||
|
||||
async_add_devices([MqttCamera(config[CONF_NAME], topic)])
|
||||
|
||||
|
||||
class MqttCamera(Camera):
|
||||
"""MQTT camera."""
|
||||
|
||||
def __init__(self, name, topic):
|
||||
"""Initialize Local File Camera component."""
|
||||
super().__init__()
|
||||
|
||||
self._name = name
|
||||
self._topic = topic
|
||||
self._qos = 0
|
||||
self._last_image = None
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_camera_image(self):
|
||||
"""Return image response."""
|
||||
return self._last_image
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of this camera."""
|
||||
return self._name
|
||||
|
||||
def async_added_to_hass(self):
|
||||
"""Subscribe mqtt events.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
@callback
|
||||
def message_received(topic, payload, qos):
|
||||
"""A new MQTT message has been received."""
|
||||
self._last_image = payload
|
||||
|
||||
return mqtt.async_subscribe(
|
||||
self.hass, self._topic, message_received, self._qos, None)
|
65
homeassistant/components/camera/neato.py
Normal file
65
homeassistant/components/camera/neato.py
Normal file
@ -0,0 +1,65 @@
|
||||
"""
|
||||
Camera that loads a picture from a local file.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/camera.neato/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from datetime import timedelta
|
||||
from homeassistant.components.camera import Camera
|
||||
from homeassistant.components.neato import (
|
||||
NEATO_MAP_DATA, NEATO_ROBOTS, NEATO_LOGIN)
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['neato']
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Camera."""
|
||||
dev = []
|
||||
for robot in hass.data[NEATO_ROBOTS]:
|
||||
if 'maps' in robot.traits:
|
||||
dev.append(NeatoCleaningMap(hass, robot))
|
||||
_LOGGER.debug('Adding robots for cleaning maps %s', dev)
|
||||
add_devices(dev, True)
|
||||
|
||||
|
||||
class NeatoCleaningMap(Camera):
|
||||
"""Neato cleaning map for last clean."""
|
||||
|
||||
def __init__(self, hass, robot):
|
||||
"""Initialize Neato cleaning map."""
|
||||
super().__init__()
|
||||
self.robot = robot
|
||||
self._robot_name = self.robot.name + ' Cleaning Map'
|
||||
self._robot_serial = self.robot.serial
|
||||
self.neato = hass.data[NEATO_LOGIN]
|
||||
self._image_url = None
|
||||
self._image = None
|
||||
|
||||
def camera_image(self):
|
||||
"""Return image response."""
|
||||
self.update()
|
||||
return self._image
|
||||
|
||||
@Throttle(timedelta(seconds=10))
|
||||
def update(self):
|
||||
"""Check the contents of the map list."""
|
||||
self.neato.update_robots()
|
||||
image_url = None
|
||||
map_data = self.hass.data[NEATO_MAP_DATA]
|
||||
image_url = map_data[self._robot_serial]['maps'][0]['url']
|
||||
if image_url == self._image_url:
|
||||
_LOGGER.debug('The map image_url is the same as old')
|
||||
return
|
||||
image = self.neato.download_map(image_url)
|
||||
self._image = image.read()
|
||||
self._image_url = image_url
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of this camera."""
|
||||
return self._robot_name
|
@ -101,11 +101,17 @@ class ZoneMinderCamera(MjpegCamera):
|
||||
status_response = zoneminder.get_state(
|
||||
'api/monitors/alarm/id:%i/command:status.json' % self._monitor_id
|
||||
)
|
||||
|
||||
if not status_response:
|
||||
_LOGGER.warning('Could not get status for monitor %i',
|
||||
self._monitor_id)
|
||||
return
|
||||
|
||||
if status_response['success'] is False:
|
||||
_LOGGER.warning('Alarm status API call failed for monitor %i',
|
||||
self._monitor_id)
|
||||
return
|
||||
|
||||
self._is_recording = status_response['status'] == ZM_STATE_ALARM
|
||||
|
||||
@property
|
||||
|
@ -180,7 +180,7 @@ class Configurator(object):
|
||||
|
||||
# field validation goes here?
|
||||
|
||||
callback(call.data.get(ATTR_FIELDS, {}))
|
||||
self.hass.async_add_job(callback, call.data.get(ATTR_FIELDS, {}))
|
||||
|
||||
def _generate_unique_id(self):
|
||||
"""Generate a unique configurator ID."""
|
||||
|
@ -20,13 +20,13 @@ _LOGGER = logging.getLogger(__name__)
|
||||
SUPPORT_GARAGE = SUPPORT_OPEN | SUPPORT_CLOSE
|
||||
|
||||
|
||||
def get_device(values, node_config, **kwargs):
|
||||
def get_device(hass, values, node_config, **kwargs):
|
||||
"""Create zwave entity device."""
|
||||
invert_buttons = node_config.get(zwave.CONF_INVERT_OPENCLOSE_BUTTONS)
|
||||
if (values.primary.command_class ==
|
||||
zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL
|
||||
and values.primary.index == 0):
|
||||
return ZwaveRollershutter(values, invert_buttons)
|
||||
return ZwaveRollershutter(hass, values, invert_buttons)
|
||||
elif (values.primary.command_class in [
|
||||
zwave.const.COMMAND_CLASS_SWITCH_BINARY,
|
||||
zwave.const.COMMAND_CLASS_BARRIER_OPERATOR]):
|
||||
@ -37,10 +37,11 @@ def get_device(values, node_config, **kwargs):
|
||||
class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
|
||||
"""Representation of an Zwave roller shutter."""
|
||||
|
||||
def __init__(self, values, invert_buttons):
|
||||
def __init__(self, hass, values, invert_buttons):
|
||||
"""Initialize the zwave rollershutter."""
|
||||
ZWaveDeviceEntity.__init__(self, values, DOMAIN)
|
||||
# pylint: disable=no-member
|
||||
self._network = hass.data[zwave.ZWAVE_NETWORK]
|
||||
self._open_id = None
|
||||
self._close_id = None
|
||||
self._current_position = None
|
||||
@ -90,11 +91,11 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
|
||||
|
||||
def open_cover(self, **kwargs):
|
||||
"""Move the roller shutter up."""
|
||||
zwave.NETWORK.manager.pressButton(self._open_id)
|
||||
self._network.manager.pressButton(self._open_id)
|
||||
|
||||
def close_cover(self, **kwargs):
|
||||
"""Move the roller shutter down."""
|
||||
zwave.NETWORK.manager.pressButton(self._close_id)
|
||||
self._network.manager.pressButton(self._close_id)
|
||||
|
||||
def set_cover_position(self, position, **kwargs):
|
||||
"""Move the roller shutter to a specific position."""
|
||||
@ -102,7 +103,7 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
|
||||
|
||||
def stop_cover(self, **kwargs):
|
||||
"""Stop the roller shutter."""
|
||||
zwave.NETWORK.manager.releaseButton(self._open_id)
|
||||
self._network.manager.releaseButton(self._open_id)
|
||||
|
||||
|
||||
class ZwaveGarageDoor(zwave.ZWaveDeviceEntity, CoverDevice):
|
||||
|
@ -159,13 +159,13 @@ def async_setup(hass, config):
|
||||
|
||||
tasks2.append(group.Group.async_create_group(hass, 'living room', [
|
||||
lights[1], switches[0], 'input_select.living_room_preset',
|
||||
'rollershutter.living_room_window', media_players[1],
|
||||
'cover.living_room_window', media_players[1],
|
||||
'scene.romantic_lights']))
|
||||
tasks2.append(group.Group.async_create_group(hass, 'bedroom', [
|
||||
lights[0], switches[1], media_players[0],
|
||||
'input_slider.noise_allowance']))
|
||||
tasks2.append(group.Group.async_create_group(hass, 'kitchen', [
|
||||
lights[2], 'rollershutter.kitchen_window', 'lock.kitchen_door']))
|
||||
lights[2], 'cover.kitchen_window', 'lock.kitchen_door']))
|
||||
tasks2.append(group.Group.async_create_group(hass, 'doors', [
|
||||
'lock.front_door', 'lock.kitchen_door',
|
||||
'garage_door.right_garage_door', 'garage_door.left_garage_door']))
|
||||
@ -176,8 +176,8 @@ def async_setup(hass, config):
|
||||
'device_tracker.demo_paulus']))
|
||||
tasks2.append(group.Group.async_create_group(hass, 'downstairs', [
|
||||
'group.living_room', 'group.kitchen',
|
||||
'scene.romantic_lights', 'rollershutter.kitchen_window',
|
||||
'rollershutter.living_room_window', 'group.doors',
|
||||
'scene.romantic_lights', 'cover.kitchen_window',
|
||||
'cover.living_room_window', 'group.doors',
|
||||
'thermostat.ecobee',
|
||||
], view=True))
|
||||
|
||||
|
@ -4,19 +4,20 @@ Support for the Automatic platform.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_tracker.automatic/
|
||||
"""
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
import re
|
||||
import requests
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.device_tracker import (
|
||||
PLATFORM_SCHEMA, ATTR_ATTRIBUTES)
|
||||
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.event import track_utc_time_change
|
||||
from homeassistant.util import datetime as dt_util
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
|
||||
REQUIREMENTS = ['aioautomatic==0.1.1']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -24,129 +25,101 @@ CONF_CLIENT_ID = 'client_id'
|
||||
CONF_SECRET = 'secret'
|
||||
CONF_DEVICES = 'devices'
|
||||
|
||||
SCOPE = 'scope:location scope:vehicle:profile scope:user:profile scope:trip'
|
||||
|
||||
ATTR_ACCESS_TOKEN = 'access_token'
|
||||
ATTR_EXPIRES_IN = 'expires_in'
|
||||
ATTR_RESULTS = 'results'
|
||||
ATTR_VEHICLE = 'vehicle'
|
||||
ATTR_ENDED_AT = 'ended_at'
|
||||
ATTR_END_LOCATION = 'end_location'
|
||||
|
||||
URL_AUTHORIZE = 'https://accounts.automatic.com/oauth/access_token/'
|
||||
URL_VEHICLES = 'https://api.automatic.com/vehicle/'
|
||||
URL_TRIPS = 'https://api.automatic.com/trip/'
|
||||
|
||||
_VEHICLE_ID_REGEX = re.compile(
|
||||
(URL_VEHICLES + '(.*)?[/]$').replace('/', r'\/'))
|
||||
DEFAULT_TIMEOUT = 5
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_CLIENT_ID): cv.string,
|
||||
vol.Required(CONF_SECRET): cv.string,
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_DEVICES): vol.All(cv.ensure_list, [cv.string])
|
||||
vol.Optional(CONF_DEVICES, default=None): vol.All(
|
||||
cv.ensure_list, [cv.string])
|
||||
})
|
||||
|
||||
|
||||
def setup_scanner(hass, config: dict, see, discovery_info=None):
|
||||
@asyncio.coroutine
|
||||
def async_setup_scanner(hass, config, async_see, discovery_info=None):
|
||||
"""Validate the configuration and return an Automatic scanner."""
|
||||
import aioautomatic
|
||||
|
||||
client = aioautomatic.Client(
|
||||
client_id=config[CONF_CLIENT_ID],
|
||||
client_secret=config[CONF_SECRET],
|
||||
client_session=async_get_clientsession(hass),
|
||||
request_kwargs={'timeout': DEFAULT_TIMEOUT})
|
||||
try:
|
||||
AutomaticDeviceScanner(hass, config, see)
|
||||
except requests.HTTPError as err:
|
||||
session = yield from client.create_session_from_password(
|
||||
config[CONF_USERNAME], config[CONF_PASSWORD])
|
||||
data = AutomaticData(hass, session, config[CONF_DEVICES], async_see)
|
||||
except aioautomatic.exceptions.AutomaticError as err:
|
||||
_LOGGER.error(str(err))
|
||||
return False
|
||||
|
||||
yield from data.update()
|
||||
return True
|
||||
|
||||
|
||||
class AutomaticDeviceScanner(object):
|
||||
"""A class representing an Automatic device."""
|
||||
class AutomaticData(object):
|
||||
"""A class representing an Automatic cloud service connection."""
|
||||
|
||||
def __init__(self, hass, config: dict, see) -> None:
|
||||
def __init__(self, hass, session, devices, async_see):
|
||||
"""Initialize the automatic device scanner."""
|
||||
self.hass = hass
|
||||
self._devices = config.get(CONF_DEVICES, None)
|
||||
self._access_token_payload = {
|
||||
'username': config.get(CONF_USERNAME),
|
||||
'password': config.get(CONF_PASSWORD),
|
||||
'client_id': config.get(CONF_CLIENT_ID),
|
||||
'client_secret': config.get(CONF_SECRET),
|
||||
'grant_type': 'password',
|
||||
'scope': SCOPE
|
||||
}
|
||||
self._headers = None
|
||||
self._token_expires = dt_util.now()
|
||||
self.last_results = {}
|
||||
self.last_trips = {}
|
||||
self.see = see
|
||||
self.devices = devices
|
||||
self.session = session
|
||||
self.async_see = async_see
|
||||
|
||||
self._update_info()
|
||||
async_track_time_interval(hass, self.update, timedelta(seconds=30))
|
||||
|
||||
track_utc_time_change(self.hass, self._update_info,
|
||||
second=range(0, 60, 30))
|
||||
|
||||
def _update_headers(self):
|
||||
"""Get the access token from automatic."""
|
||||
if self._headers is None or self._token_expires <= dt_util.now():
|
||||
resp = requests.post(
|
||||
URL_AUTHORIZE,
|
||||
data=self._access_token_payload)
|
||||
|
||||
resp.raise_for_status()
|
||||
|
||||
json = resp.json()
|
||||
|
||||
access_token = json[ATTR_ACCESS_TOKEN]
|
||||
self._token_expires = dt_util.now() + timedelta(
|
||||
seconds=json[ATTR_EXPIRES_IN])
|
||||
self._headers = {
|
||||
'Authorization': 'Bearer {}'.format(access_token)
|
||||
}
|
||||
|
||||
def _update_info(self, now=None) -> None:
|
||||
@asyncio.coroutine
|
||||
def update(self, now=None):
|
||||
"""Update the device info."""
|
||||
import aioautomatic
|
||||
|
||||
_LOGGER.debug('Updating devices %s', now)
|
||||
self._update_headers()
|
||||
|
||||
response = requests.get(URL_VEHICLES, headers=self._headers)
|
||||
try:
|
||||
vehicles = yield from self.session.get_vehicles()
|
||||
except aioautomatic.exceptions.AutomaticError as err:
|
||||
_LOGGER.error(str(err))
|
||||
return False
|
||||
|
||||
response.raise_for_status()
|
||||
for vehicle in vehicles:
|
||||
name = vehicle.display_name
|
||||
if name is None:
|
||||
name = ' '.join(filter(None, (
|
||||
str(vehicle.year), vehicle.make, vehicle.model)))
|
||||
|
||||
self.last_results = [item for item in response.json()[ATTR_RESULTS]
|
||||
if self._devices is None or item[
|
||||
'display_name'] in self._devices]
|
||||
if self.devices is not None and name not in self.devices:
|
||||
continue
|
||||
|
||||
response = requests.get(URL_TRIPS, headers=self._headers)
|
||||
self.hass.async_add_job(self.update_vehicle(vehicle, name))
|
||||
|
||||
if response.status_code == 200:
|
||||
for trip in response.json()[ATTR_RESULTS]:
|
||||
vehicle_id = _VEHICLE_ID_REGEX.match(
|
||||
trip[ATTR_VEHICLE]).group(1)
|
||||
if vehicle_id not in self.last_trips:
|
||||
self.last_trips[vehicle_id] = trip
|
||||
elif self.last_trips[vehicle_id][ATTR_ENDED_AT] < trip[
|
||||
ATTR_ENDED_AT]:
|
||||
self.last_trips[vehicle_id] = trip
|
||||
@asyncio.coroutine
|
||||
def update_vehicle(self, vehicle, name):
|
||||
"""Updated the specified vehicle's data."""
|
||||
import aioautomatic
|
||||
|
||||
for vehicle in self.last_results:
|
||||
dev_id = vehicle.get('id')
|
||||
host_name = vehicle.get('display_name')
|
||||
kwargs = {
|
||||
'dev_id': vehicle.id,
|
||||
'host_name': name,
|
||||
'mac': vehicle.id,
|
||||
ATTR_ATTRIBUTES: {
|
||||
'fuel_level': vehicle.fuel_level_percent,
|
||||
}
|
||||
}
|
||||
|
||||
attrs = {
|
||||
'fuel_level': vehicle.get('fuel_level_percent')
|
||||
}
|
||||
trips = []
|
||||
try:
|
||||
# Get the most recent trip for this vehicle
|
||||
trips = yield from self.session.get_trips(
|
||||
vehicle=vehicle.id, limit=1)
|
||||
except aioautomatic.exceptions.AutomaticError as err:
|
||||
_LOGGER.error(str(err))
|
||||
|
||||
kwargs = {
|
||||
'dev_id': dev_id,
|
||||
'host_name': host_name,
|
||||
'mac': dev_id,
|
||||
ATTR_ATTRIBUTES: attrs
|
||||
}
|
||||
if trips:
|
||||
end_location = trips[0].end_location
|
||||
kwargs['gps'] = (end_location.lat, end_location.lon)
|
||||
kwargs['gps_accuracy'] = end_location.accuracy_m
|
||||
|
||||
if dev_id in self.last_trips:
|
||||
end_location = self.last_trips[dev_id][ATTR_END_LOCATION]
|
||||
kwargs['gps'] = (end_location['lat'], end_location['lon'])
|
||||
kwargs['gps_accuracy'] = end_location['accuracy_m']
|
||||
|
||||
self.see(**kwargs)
|
||||
yield from self.async_see(**kwargs)
|
||||
|
@ -22,20 +22,20 @@ URL = '/api/locative'
|
||||
|
||||
|
||||
def setup_scanner(hass, config, see, discovery_info=None):
|
||||
"""Setup an endpoint for the Locative application."""
|
||||
"""Set up an endpoint for the Locative application."""
|
||||
hass.http.register_view(LocativeView(see))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class LocativeView(HomeAssistantView):
|
||||
"""View to handle locative requests."""
|
||||
"""View to handle Locative requests."""
|
||||
|
||||
url = URL
|
||||
name = 'api:locative'
|
||||
|
||||
def __init__(self, see):
|
||||
"""Initialize Locative url endpoints."""
|
||||
"""Initialize Locative URL endpoints."""
|
||||
self.see = see
|
||||
|
||||
@asyncio.coroutine
|
||||
@ -52,7 +52,6 @@ class LocativeView(HomeAssistantView):
|
||||
return res
|
||||
|
||||
@asyncio.coroutine
|
||||
# pylint: disable=too-many-return-statements
|
||||
def _handle(self, hass, data):
|
||||
"""Handle locative request."""
|
||||
if 'latitude' not in data or 'longitude' not in data:
|
||||
|
85
homeassistant/components/device_tracker/mqtt_json.py
Normal file
85
homeassistant/components/device_tracker/mqtt_json.py
Normal file
@ -0,0 +1,85 @@
|
||||
"""
|
||||
Support for GPS tracking MQTT enabled devices.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_tracker.mqtt_json/
|
||||
"""
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.components.mqtt as mqtt
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.components.mqtt import CONF_QOS
|
||||
from homeassistant.components.device_tracker import PLATFORM_SCHEMA
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.const import (
|
||||
CONF_DEVICES, ATTR_GPS_ACCURACY, ATTR_LATITUDE,
|
||||
ATTR_LONGITUDE, ATTR_BATTERY_LEVEL)
|
||||
|
||||
DEPENDENCIES = ['mqtt']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
GPS_JSON_PAYLOAD_SCHEMA = vol.Schema({
|
||||
vol.Required(ATTR_LATITUDE): vol.Coerce(float),
|
||||
vol.Required(ATTR_LONGITUDE): vol.Coerce(float),
|
||||
vol.Optional(ATTR_GPS_ACCURACY, default=None): vol.Coerce(int),
|
||||
vol.Optional(ATTR_BATTERY_LEVEL, default=None): vol.Coerce(str),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(mqtt.SCHEMA_BASE).extend({
|
||||
vol.Required(CONF_DEVICES): {cv.string: mqtt.valid_subscribe_topic},
|
||||
})
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_scanner(hass, config, async_see, discovery_info=None):
|
||||
"""Setup the MQTT tracker."""
|
||||
devices = config[CONF_DEVICES]
|
||||
qos = config[CONF_QOS]
|
||||
|
||||
dev_id_lookup = {}
|
||||
|
||||
@callback
|
||||
def async_tracker_message_received(topic, payload, qos):
|
||||
"""MQTT message received."""
|
||||
dev_id = dev_id_lookup[topic]
|
||||
|
||||
try:
|
||||
data = GPS_JSON_PAYLOAD_SCHEMA(json.loads(payload))
|
||||
except vol.MultipleInvalid:
|
||||
_LOGGER.error('Skipping update for following data '
|
||||
'because of missing or malformatted data: %s',
|
||||
payload)
|
||||
return
|
||||
except ValueError:
|
||||
_LOGGER.error('Error parsing JSON payload: %s', payload)
|
||||
return
|
||||
|
||||
kwargs = _parse_see_args(dev_id, data)
|
||||
hass.async_add_job(
|
||||
async_see(**kwargs))
|
||||
|
||||
for dev_id, topic in devices.items():
|
||||
dev_id_lookup[topic] = dev_id
|
||||
yield from mqtt.async_subscribe(
|
||||
hass, topic, async_tracker_message_received, qos)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def _parse_see_args(dev_id, data):
|
||||
"""Parse the payload location parameters, into the format see expects."""
|
||||
kwargs = {
|
||||
'gps': (data[ATTR_LATITUDE], data[ATTR_LONGITUDE]),
|
||||
'dev_id': dev_id
|
||||
}
|
||||
|
||||
if ATTR_GPS_ACCURACY in data:
|
||||
kwargs[ATTR_GPS_ACCURACY] = data[ATTR_GPS_ACCURACY]
|
||||
if ATTR_BATTERY_LEVEL in data:
|
||||
kwargs['battery'] = data[ATTR_BATTERY_LEVEL]
|
||||
return kwargs
|
@ -1,15 +1,8 @@
|
||||
"""
|
||||
Tracks devices by sending a ICMP ping.
|
||||
Tracks devices by sending a ICMP echo request (ping).
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_tracker.ping/
|
||||
|
||||
device_tracker:
|
||||
- platform: ping
|
||||
count: 2
|
||||
hosts:
|
||||
host_one: pc.local
|
||||
host_two: 192.168.2.25
|
||||
"""
|
||||
import logging
|
||||
import subprocess
|
||||
@ -18,14 +11,12 @@ from datetime import timedelta
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.device_tracker import (
|
||||
PLATFORM_SCHEMA, DEFAULT_SCAN_INTERVAL, SOURCE_TYPE_ROUTER)
|
||||
from homeassistant.helpers.event import track_point_in_utc_time
|
||||
from homeassistant import util
|
||||
from homeassistant import const
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
DEPENDENCIES = []
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -37,7 +28,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
})
|
||||
|
||||
|
||||
class Host:
|
||||
class Host(object):
|
||||
"""Host object with ping detection."""
|
||||
|
||||
def __init__(self, ip_address, dev_id, hass, config):
|
||||
@ -53,8 +44,10 @@ class Host:
|
||||
self.ip_address]
|
||||
|
||||
def ping(self):
|
||||
"""Send ICMP ping and return True if success."""
|
||||
pinger = subprocess.Popen(self._ping_cmd, stdout=subprocess.PIPE)
|
||||
"""Send an ICMP echo request and return True if success."""
|
||||
pinger = subprocess.Popen(self._ping_cmd,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.DEVNULL)
|
||||
try:
|
||||
pinger.communicate()
|
||||
return pinger.returncode == 0
|
||||
@ -70,7 +63,7 @@ class Host:
|
||||
return True
|
||||
failed += 1
|
||||
|
||||
_LOGGER.debug("ping KO on ip=%s failed=%d", self.ip_address, failed)
|
||||
_LOGGER.debug("No response from %s failed=%d", self.ip_address, failed)
|
||||
|
||||
|
||||
def setup_scanner(hass, config, see, discovery_info=None):
|
||||
|
@ -10,6 +10,7 @@ import asyncio
|
||||
import json
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
import os
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@ -20,7 +21,7 @@ from homeassistant.helpers.event import async_track_point_in_utc_time
|
||||
from homeassistant.helpers.discovery import async_load_platform, async_discover
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
REQUIREMENTS = ['netdisco==0.9.2']
|
||||
REQUIREMENTS = ['netdisco==1.0.0rc3']
|
||||
|
||||
DOMAIN = 'discovery'
|
||||
|
||||
@ -28,11 +29,15 @@ SCAN_INTERVAL = timedelta(seconds=300)
|
||||
SERVICE_NETGEAR = 'netgear_router'
|
||||
SERVICE_WEMO = 'belkin_wemo'
|
||||
SERVICE_HASS_IOS_APP = 'hass_ios'
|
||||
SERVICE_IKEA_TRADFRI = 'ikea_tradfri'
|
||||
SERVICE_HASSIO = 'hassio'
|
||||
|
||||
SERVICE_HANDLERS = {
|
||||
SERVICE_HASS_IOS_APP: ('ios', None),
|
||||
SERVICE_NETGEAR: ('device_tracker', None),
|
||||
SERVICE_WEMO: ('wemo', None),
|
||||
SERVICE_IKEA_TRADFRI: ('tradfri', None),
|
||||
SERVICE_HASSIO: ('hassio', None),
|
||||
'philips_hue': ('light', 'hue'),
|
||||
'google_cast': ('media_player', 'cast'),
|
||||
'panasonic_viera': ('media_player', 'panasonic_viera'),
|
||||
@ -45,10 +50,10 @@ SERVICE_HANDLERS = {
|
||||
'denonavr': ('media_player', 'denonavr'),
|
||||
'samsung_tv': ('media_player', 'samsungtv'),
|
||||
'yeelight': ('light', 'yeelight'),
|
||||
'flux_led': ('light', 'flux_led'),
|
||||
'apple_tv': ('media_player', 'apple_tv'),
|
||||
'frontier_silicon': ('media_player', 'frontier_silicon'),
|
||||
'openhome': ('media_player', 'openhome'),
|
||||
'bose_soundtouch': ('media_player', 'soundtouch'),
|
||||
}
|
||||
|
||||
CONF_IGNORE = 'ignore'
|
||||
@ -122,6 +127,10 @@ def async_setup(hass, config):
|
||||
"""Schedule the first discovery when Home Assistant starts up."""
|
||||
async_track_point_in_utc_time(hass, scan_devices, dt_util.utcnow())
|
||||
|
||||
# discovery local services
|
||||
if 'HASSIO' in os.environ:
|
||||
hass.async_add_job(new_service_found(SERVICE_HASSIO, {}))
|
||||
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, schedule_first)
|
||||
|
||||
return True
|
||||
|
@ -3,18 +3,19 @@
|
||||
FINGERPRINTS = {
|
||||
"compatibility.js": "83d9c77748dafa9db49ae77d7f3d8fb0",
|
||||
"core.js": "5d08475f03adb5969bd31855d5ca0cfd",
|
||||
"frontend.html": "feaf3e9453eca239f29eb10e7645a84f",
|
||||
"mdi.html": "989f02c51eba561dc32b9ecc628a84b3",
|
||||
"frontend.html": "8264c0ee8dafb09785ec7b934795d3b1",
|
||||
"mdi.html": "d86ee142ae2476f49384bfe866a2885e",
|
||||
"micromarkdown-js.html": "93b5ec4016f0bba585521cf4d18dec1a",
|
||||
"panels/ha-panel-config.html": "6dcb246cd356307a638f81c4f89bf9b3",
|
||||
"panels/ha-panel-dev-event.html": "1f169700c2345785855b1d7919d12326",
|
||||
"panels/ha-panel-config.html": "0b42cb4e709ce35ad2666ffeca6f9b14",
|
||||
"panels/ha-panel-dev-event.html": "2db9c218065ef0f61d8d08db8093cad2",
|
||||
"panels/ha-panel-dev-info.html": "61610e015a411cfc84edd2c4d489e71d",
|
||||
"panels/ha-panel-dev-service.html": "0fe8e6acdccf2dc3d1ae657b2c7f2df0",
|
||||
"panels/ha-panel-dev-state.html": "48d37db4a1d6708314ded1d624d0f4d4",
|
||||
"panels/ha-panel-dev-template.html": "6f353392d68574fbc5af188bca44d0ae",
|
||||
"panels/ha-panel-history.html": "6945cebe5d8075aae560d2c4246b063f",
|
||||
"panels/ha-panel-dev-service.html": "415552027cb083badeff5f16080410ed",
|
||||
"panels/ha-panel-dev-state.html": "d70314913b8923d750932367b1099750",
|
||||
"panels/ha-panel-dev-template.html": "567fbf86735e1b891e40c2f4060fec9b",
|
||||
"panels/ha-panel-history.html": "be115906882752d220199abbaddc53e5",
|
||||
"panels/ha-panel-iframe.html": "d920f0aa3c903680f2f8795e2255daab",
|
||||
"panels/ha-panel-logbook.html": "a1fc2b5d739bedb9d87e4da4cd929a71",
|
||||
"panels/ha-panel-map.html": "e3c7a54f90dd4269d7e53cdcd96514c6",
|
||||
"panels/ha-panel-logbook.html": "bf29de0c586a598113c6cc09ead12b00",
|
||||
"panels/ha-panel-map.html": "31c592c239636f91e07c7ac232a5ebc4",
|
||||
"panels/ha-panel-zwave.html": "f52d0c001f48e0c7b33a172f3a71b547",
|
||||
"websocket_test.html": "575de64b431fe11c3785bf96d7813450"
|
||||
}
|
||||
|
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
@ -1 +1 @@
|
||||
Subproject commit abbdc6f055524c5d3ed0bb50e35400fed40d573f
|
||||
Subproject commit 3fdba359865823805e8ea756c8500d3913976158
|
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
@ -31,9 +31,11 @@ SERVICE_HOST_SHUTDOWN = 'host_shutdown'
|
||||
SERVICE_HOST_REBOOT = 'host_reboot'
|
||||
|
||||
SERVICE_HOST_UPDATE = 'host_update'
|
||||
SERVICE_SUPERVISOR_UPDATE = 'supervisor_update'
|
||||
SERVICE_HOMEASSISTANT_UPDATE = 'homeassistant_update'
|
||||
|
||||
SERVICE_SUPERVISOR_UPDATE = 'supervisor_update'
|
||||
SERVICE_SUPERVISOR_RELOAD = 'supervisor_reload'
|
||||
|
||||
SERVICE_ADDON_INSTALL = 'addon_install'
|
||||
SERVICE_ADDON_UNINSTALL = 'addon_uninstall'
|
||||
SERVICE_ADDON_UPDATE = 'addon_update'
|
||||
@ -61,8 +63,9 @@ SERVICE_MAP = {
|
||||
SERVICE_HOST_SHUTDOWN: None,
|
||||
SERVICE_HOST_REBOOT: None,
|
||||
SERVICE_HOST_UPDATE: SCHEMA_SERVICE_UPDATE,
|
||||
SERVICE_SUPERVISOR_UPDATE: SCHEMA_SERVICE_UPDATE,
|
||||
SERVICE_HOMEASSISTANT_UPDATE: SCHEMA_SERVICE_UPDATE,
|
||||
SERVICE_SUPERVISOR_UPDATE: SCHEMA_SERVICE_UPDATE,
|
||||
SERVICE_SUPERVISOR_RELOAD: None,
|
||||
SERVICE_ADDON_INSTALL: SCHEMA_SERVICE_ADDONS_VERSION,
|
||||
SERVICE_ADDON_UNINSTALL: SCHEMA_SERVICE_ADDONS,
|
||||
SERVICE_ADDON_START: SCHEMA_SERVICE_ADDONS,
|
||||
@ -117,6 +120,9 @@ def async_setup(hass, config):
|
||||
elif service.service == SERVICE_SUPERVISOR_UPDATE:
|
||||
yield from hassio.send_command(
|
||||
"/supervisor/update", payload=version)
|
||||
elif service.service == SERVICE_SUPERVISOR_RELOAD:
|
||||
yield from hassio.send_command(
|
||||
"/supervisor/reload", timeout=LONG_TASK_TIMEOUT)
|
||||
elif service.service == SERVICE_HOMEASSISTANT_UPDATE:
|
||||
yield from hassio.send_command(
|
||||
"/homeassistant/update", payload=version,
|
||||
@ -131,7 +137,8 @@ def async_setup(hass, config):
|
||||
elif service.service == SERVICE_ADDON_START:
|
||||
yield from hassio.send_command("/addons/{}/start".format(addon))
|
||||
elif service.service == SERVICE_ADDON_STOP:
|
||||
yield from hassio.send_command("/addons/{}/stop".format(addon))
|
||||
yield from hassio.send_command(
|
||||
"/addons/{}/stop".format(addon), timeout=LONG_TASK_TIMEOUT)
|
||||
elif service.service == SERVICE_ADDON_UPDATE:
|
||||
yield from hassio.send_command(
|
||||
"/addons/{}/update".format(addon), payload=version,
|
||||
@ -168,8 +175,10 @@ class HassIO(object):
|
||||
@asyncio.coroutine
|
||||
def send_command(self, cmd, payload=None, timeout=DEFAULT_TIMEOUT):
|
||||
"""Send request to API."""
|
||||
answer = yield from self.send_raw(cmd, payload=payload)
|
||||
if answer['result'] == 'ok':
|
||||
answer = yield from self.send_raw(
|
||||
cmd, payload=payload, timeout=timeout
|
||||
)
|
||||
if answer and answer['result'] == 'ok':
|
||||
return answer['data'] if answer['data'] else True
|
||||
|
||||
_LOGGER.error("%s return error %s.", cmd, answer['message'])
|
||||
|
@ -8,6 +8,7 @@ import asyncio
|
||||
import os
|
||||
import json
|
||||
import logging
|
||||
import datetime
|
||||
|
||||
import voluptuous as vol
|
||||
from voluptuous.humanize import humanize_error
|
||||
@ -20,6 +21,8 @@ from homeassistant.core import callback
|
||||
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
|
||||
from homeassistant.remote import JSONEncoder
|
||||
|
||||
from homeassistant.const import (HTTP_INTERNAL_SERVER_ERROR,
|
||||
HTTP_BAD_REQUEST)
|
||||
|
||||
@ -55,6 +58,8 @@ ATTR_TEXT_INPUT_BEHAVIOR = "textInput"
|
||||
|
||||
BEHAVIORS = [ATTR_DEFAULT_BEHAVIOR, ATTR_TEXT_INPUT_BEHAVIOR]
|
||||
|
||||
ATTR_LAST_SEEN_AT = "lastSeenAt"
|
||||
|
||||
ATTR_DEVICE = "device"
|
||||
ATTR_PUSH_TOKEN = "pushToken"
|
||||
ATTR_APP = "app"
|
||||
@ -192,7 +197,7 @@ def _save_config(filename, config):
|
||||
"""Save configuration."""
|
||||
try:
|
||||
with open(filename, "w") as fdesc:
|
||||
fdesc.write(json.dumps(config))
|
||||
fdesc.write(json.dumps(config, cls=JSONEncoder))
|
||||
except (IOError, TypeError) as error:
|
||||
_LOGGER.error("Saving config file failed: %s", error)
|
||||
return False
|
||||
@ -285,7 +290,7 @@ class iOSIdentifyDeviceView(HomeAssistantView):
|
||||
try:
|
||||
req_data = yield from request.json()
|
||||
except ValueError:
|
||||
return self.json_message('Invalid JSON', HTTP_BAD_REQUEST)
|
||||
return self.json_message("Invalid JSON", HTTP_BAD_REQUEST)
|
||||
|
||||
try:
|
||||
data = IDENTIFY_SCHEMA(req_data)
|
||||
@ -293,6 +298,8 @@ class iOSIdentifyDeviceView(HomeAssistantView):
|
||||
return self.json_message(humanize_error(request.json, ex),
|
||||
HTTP_BAD_REQUEST)
|
||||
|
||||
data[ATTR_LAST_SEEN_AT] = datetime.datetime.now()
|
||||
|
||||
name = data.get(ATTR_DEVICE_ID)
|
||||
|
||||
CONFIG_FILE[ATTR_DEVICES][name] = data
|
||||
|
@ -108,20 +108,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
lights.append(light)
|
||||
light_ips.append(ipaddr)
|
||||
|
||||
if discovery_info:
|
||||
device = {}
|
||||
# discovery_info: ip address,device id,device type
|
||||
device['ipaddr'] = discovery_info[0]
|
||||
device['name'] = discovery_info[1]
|
||||
# As we don't know protocol and mode set to none to autodetect.
|
||||
device[CONF_PROTOCOL] = None
|
||||
device[ATTR_MODE] = None
|
||||
|
||||
light = FluxLight(device)
|
||||
if light.is_valid:
|
||||
lights.append(light)
|
||||
light_ips.append(device['ipaddr'])
|
||||
|
||||
if not config.get(CONF_AUTOMATIC_ADD, False):
|
||||
add_devices(lights)
|
||||
return
|
||||
@ -230,9 +216,9 @@ class FluxLight(Light):
|
||||
(red, green, blue) = self._bulb.getRgb()
|
||||
self._bulb.setRgb(red, green, blue, brightness=brightness)
|
||||
elif effect == EFFECT_RANDOM:
|
||||
self._bulb.setRgb(random.randrange(0, 255),
|
||||
random.randrange(0, 255),
|
||||
random.randrange(0, 255))
|
||||
self._bulb.setRgb(random.randint(0, 255),
|
||||
random.randint(0, 255),
|
||||
random.randint(0, 255))
|
||||
elif effect == EFFECT_COLORLOOP:
|
||||
self._bulb.setPresetPattern(0x25, 50)
|
||||
elif effect == EFFECT_RED_FADE:
|
||||
|
@ -10,7 +10,6 @@ import os
|
||||
import random
|
||||
import socket
|
||||
from datetime import timedelta
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@ -115,11 +114,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
allow_hue_groups = config.get(CONF_ALLOW_HUE_GROUPS)
|
||||
|
||||
if discovery_info is not None:
|
||||
host = urlparse(discovery_info[1]).hostname
|
||||
|
||||
if "HASS Bridge" in discovery_info[0]:
|
||||
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)
|
||||
|
||||
|
@ -10,19 +10,24 @@ import asyncio
|
||||
import sys
|
||||
from functools import partial
|
||||
from datetime import timedelta
|
||||
import async_timeout
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_RGB_COLOR, ATTR_TRANSITION,
|
||||
Light, PLATFORM_SCHEMA, ATTR_BRIGHTNESS, ATTR_COLOR_NAME, ATTR_RGB_COLOR,
|
||||
ATTR_XY_COLOR, ATTR_COLOR_TEMP, ATTR_TRANSITION, ATTR_EFFECT,
|
||||
SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_RGB_COLOR,
|
||||
SUPPORT_TRANSITION, Light, PLATFORM_SCHEMA)
|
||||
SUPPORT_XY_COLOR, SUPPORT_TRANSITION, SUPPORT_EFFECT)
|
||||
from homeassistant.util.color import (
|
||||
color_temperature_mired_to_kelvin, color_temperature_kelvin_to_mired)
|
||||
from homeassistant import util
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.event import async_track_point_in_utc_time
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
import homeassistant.util.color as color_util
|
||||
|
||||
from . import effects as lifx_effects
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -35,18 +40,19 @@ BULB_LATENCY = 500
|
||||
|
||||
CONF_SERVER = 'server'
|
||||
|
||||
ATTR_HSBK = 'hsbk'
|
||||
|
||||
BYTE_MAX = 255
|
||||
SHORT_MAX = 65535
|
||||
|
||||
SUPPORT_LIFX = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_RGB_COLOR |
|
||||
SUPPORT_TRANSITION)
|
||||
SUPPORT_XY_COLOR | SUPPORT_TRANSITION | SUPPORT_EFFECT)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_SERVER, default='0.0.0.0'): cv.string,
|
||||
})
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Setup the LIFX platform."""
|
||||
@ -65,6 +71,9 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
local_addr=(server_addr, UDP_BROADCAST_PORT))
|
||||
|
||||
hass.async_add_job(coro)
|
||||
|
||||
lifx_effects.setup(hass, lifx_manager)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@ -82,9 +91,8 @@ class LIFXManager(object):
|
||||
"""Callback for newly detected bulb."""
|
||||
if device.mac_addr in self.entities:
|
||||
entity = self.entities[device.mac_addr]
|
||||
_LOGGER.debug("%s register AGAIN", entity.ipaddr)
|
||||
entity.available = True
|
||||
entity.device = device
|
||||
_LOGGER.debug("%s register AGAIN", entity.who)
|
||||
self.hass.async_add_job(entity.async_update_ha_state())
|
||||
else:
|
||||
_LOGGER.debug("%s register NEW", device.ip_addr)
|
||||
@ -94,7 +102,7 @@ class LIFXManager(object):
|
||||
def ready(self, device, msg):
|
||||
"""Callback that adds the device once all data is retrieved."""
|
||||
entity = LIFXLight(device)
|
||||
_LOGGER.debug("%s register READY", entity.ipaddr)
|
||||
_LOGGER.debug("%s register READY", entity.who)
|
||||
self.entities[device.mac_addr] = entity
|
||||
self.async_add_devices([entity])
|
||||
|
||||
@ -103,12 +111,44 @@ class LIFXManager(object):
|
||||
"""Callback for disappearing bulb."""
|
||||
if device.mac_addr in self.entities:
|
||||
entity = self.entities[device.mac_addr]
|
||||
_LOGGER.debug("%s unregister", entity.ipaddr)
|
||||
entity.available = False
|
||||
entity.updated_event.set()
|
||||
_LOGGER.debug("%s unregister", entity.who)
|
||||
entity.device = None
|
||||
self.hass.async_add_job(entity.async_update_ha_state())
|
||||
|
||||
|
||||
class AwaitAioLIFX:
|
||||
"""Wait for an aiolifx callback and return the message."""
|
||||
|
||||
def __init__(self, light):
|
||||
"""Initialize the wrapper."""
|
||||
self.light = light
|
||||
self.device = None
|
||||
self.message = None
|
||||
self.event = asyncio.Event()
|
||||
|
||||
@callback
|
||||
def callback(self, device, message):
|
||||
"""Callback that aiolifx invokes when the response is received."""
|
||||
self.device = device
|
||||
self.message = message
|
||||
self.event.set()
|
||||
|
||||
@asyncio.coroutine
|
||||
def wait(self, method):
|
||||
"""Call an aiolifx method and wait for its response or a timeout."""
|
||||
self.event.clear()
|
||||
method(self.callback)
|
||||
|
||||
while self.light.available and not self.event.is_set():
|
||||
try:
|
||||
with async_timeout.timeout(1.0, loop=self.light.hass.loop):
|
||||
yield from self.event.wait()
|
||||
except asyncio.TimeoutError:
|
||||
pass
|
||||
|
||||
return self.message
|
||||
|
||||
|
||||
def convert_rgb_to_hsv(rgb):
|
||||
"""Convert Home Assistant RGB values to HSV values."""
|
||||
red, green, blue = [_ / BYTE_MAX for _ in rgb]
|
||||
@ -126,32 +166,30 @@ class LIFXLight(Light):
|
||||
def __init__(self, device):
|
||||
"""Initialize the light."""
|
||||
self.device = device
|
||||
self.updated_event = asyncio.Event()
|
||||
self.blocker = None
|
||||
self.effect_data = None
|
||||
self.postponed_update = None
|
||||
self._available = True
|
||||
self._name = device.label
|
||||
self.set_power(device.power_level)
|
||||
self.set_color(*device.color)
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return the availability of the device."""
|
||||
return self._available
|
||||
|
||||
@available.setter
|
||||
def available(self, value):
|
||||
"""Set the availability of the device."""
|
||||
self._available = value
|
||||
return self.device is not None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
return self.device.label
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def ipaddr(self):
|
||||
"""Return the IP address of the device."""
|
||||
return self.device.ip_addr[0]
|
||||
def who(self):
|
||||
"""Return a string identifying the device."""
|
||||
ip_addr = '-'
|
||||
if self.device:
|
||||
ip_addr = self.device.ip_addr[0]
|
||||
return "%s (%s)" % (ip_addr, self.name)
|
||||
|
||||
@property
|
||||
def rgb_color(self):
|
||||
@ -181,11 +219,21 @@ class LIFXLight(Light):
|
||||
_LOGGER.debug("is_on: %d", self._power)
|
||||
return self._power != 0
|
||||
|
||||
@property
|
||||
def effect(self):
|
||||
"""Return the currently running effect."""
|
||||
return self.effect_data.effect.name if self.effect_data else None
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Flag supported features."""
|
||||
return SUPPORT_LIFX
|
||||
|
||||
@property
|
||||
def effect_list(self):
|
||||
"""Return the list of supported effects."""
|
||||
return lifx_effects.effect_list()
|
||||
|
||||
@callback
|
||||
def update_after_transition(self, now):
|
||||
"""Request new status after completion of the last transition."""
|
||||
@ -216,38 +264,20 @@ class LIFXLight(Light):
|
||||
@asyncio.coroutine
|
||||
def async_turn_on(self, **kwargs):
|
||||
"""Turn the device on."""
|
||||
yield from self.stop_effect()
|
||||
|
||||
if ATTR_EFFECT in kwargs:
|
||||
yield from lifx_effects.default_effect(self, **kwargs)
|
||||
return
|
||||
|
||||
if ATTR_TRANSITION in kwargs:
|
||||
fade = int(kwargs[ATTR_TRANSITION] * 1000)
|
||||
else:
|
||||
fade = 0
|
||||
|
||||
changed_color = False
|
||||
|
||||
if ATTR_RGB_COLOR in kwargs:
|
||||
hue, saturation, brightness = \
|
||||
convert_rgb_to_hsv(kwargs[ATTR_RGB_COLOR])
|
||||
changed_color = True
|
||||
else:
|
||||
hue = self._hue
|
||||
saturation = self._sat
|
||||
brightness = self._bri
|
||||
|
||||
if ATTR_BRIGHTNESS in kwargs:
|
||||
brightness = kwargs[ATTR_BRIGHTNESS] * (BYTE_MAX + 1)
|
||||
changed_color = True
|
||||
else:
|
||||
brightness = self._bri
|
||||
|
||||
if ATTR_COLOR_TEMP in kwargs:
|
||||
kelvin = int(color_temperature_mired_to_kelvin(
|
||||
kwargs[ATTR_COLOR_TEMP]))
|
||||
changed_color = True
|
||||
else:
|
||||
kelvin = self._kel
|
||||
|
||||
hsbk = [hue, saturation, brightness, kelvin]
|
||||
hsbk, changed_color = self.find_hsbk(**kwargs)
|
||||
_LOGGER.debug("turn_on: %s (%d) %d %d %d %d %d",
|
||||
self.ipaddr, self._power, fade, *hsbk)
|
||||
self.who, self._power, fade, *hsbk)
|
||||
|
||||
if self._power == 0:
|
||||
if changed_color:
|
||||
@ -266,6 +296,8 @@ class LIFXLight(Light):
|
||||
@asyncio.coroutine
|
||||
def async_turn_off(self, **kwargs):
|
||||
"""Turn the device off."""
|
||||
yield from self.stop_effect()
|
||||
|
||||
if ATTR_TRANSITION in kwargs:
|
||||
fade = int(kwargs[ATTR_TRANSITION] * 1000)
|
||||
else:
|
||||
@ -277,21 +309,71 @@ class LIFXLight(Light):
|
||||
if fade < BULB_LATENCY:
|
||||
self.set_power(0)
|
||||
|
||||
@callback
|
||||
def got_color(self, device, msg):
|
||||
"""Callback that gets current power/color status."""
|
||||
self.set_power(device.power_level)
|
||||
self.set_color(*device.color)
|
||||
self.updated_event.set()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_update(self):
|
||||
"""Update bulb status (if it is available)."""
|
||||
_LOGGER.debug("%s async_update", self.ipaddr)
|
||||
_LOGGER.debug("%s async_update", self.who)
|
||||
if self.available and self.blocker is None:
|
||||
self.updated_event.clear()
|
||||
self.device.get_color(self.got_color)
|
||||
yield from self.updated_event.wait()
|
||||
yield from self.refresh_state()
|
||||
|
||||
@asyncio.coroutine
|
||||
def stop_effect(self):
|
||||
"""Stop the currently running effect (if any)."""
|
||||
if self.effect_data:
|
||||
yield from self.effect_data.effect.async_restore(self)
|
||||
|
||||
@asyncio.coroutine
|
||||
def refresh_state(self):
|
||||
"""Ask the device about its current state and update our copy."""
|
||||
msg = yield from AwaitAioLIFX(self).wait(self.device.get_color)
|
||||
if msg is not None:
|
||||
self.set_power(self.device.power_level)
|
||||
self.set_color(*self.device.color)
|
||||
self._name = self.device.label
|
||||
|
||||
def find_hsbk(self, **kwargs):
|
||||
"""Find the desired color from a number of possible inputs."""
|
||||
changed_color = False
|
||||
|
||||
hsbk = kwargs.pop(ATTR_HSBK, None)
|
||||
if hsbk is not None:
|
||||
return [hsbk, True]
|
||||
|
||||
color_name = kwargs.pop(ATTR_COLOR_NAME, None)
|
||||
if color_name is not None:
|
||||
kwargs[ATTR_RGB_COLOR] = color_util.color_name_to_rgb(color_name)
|
||||
|
||||
if ATTR_RGB_COLOR in kwargs:
|
||||
hue, saturation, brightness = \
|
||||
convert_rgb_to_hsv(kwargs[ATTR_RGB_COLOR])
|
||||
changed_color = True
|
||||
else:
|
||||
hue = self._hue
|
||||
saturation = self._sat
|
||||
brightness = self._bri
|
||||
|
||||
if ATTR_BRIGHTNESS in kwargs:
|
||||
brightness = kwargs[ATTR_BRIGHTNESS] * (BYTE_MAX + 1)
|
||||
changed_color = True
|
||||
else:
|
||||
brightness = self._bri
|
||||
|
||||
if ATTR_XY_COLOR in kwargs:
|
||||
hue, saturation, _ = \
|
||||
color_util.color_xy_brightness_to_hsv(
|
||||
*kwargs[ATTR_XY_COLOR],
|
||||
ibrightness=(brightness // (BYTE_MAX + 1)))
|
||||
saturation = saturation * (BYTE_MAX + 1)
|
||||
changed_color = True
|
||||
|
||||
if ATTR_COLOR_TEMP in kwargs:
|
||||
kelvin = int(color_temperature_mired_to_kelvin(
|
||||
kwargs[ATTR_COLOR_TEMP]))
|
||||
changed_color = True
|
||||
else:
|
||||
kelvin = self._kel
|
||||
|
||||
return [[hue, saturation, brightness, kelvin], changed_color]
|
||||
|
||||
def set_power(self, power):
|
||||
"""Set power state value."""
|
338
homeassistant/components/light/lifx/effects.py
Normal file
338
homeassistant/components/light/lifx/effects.py
Normal file
@ -0,0 +1,338 @@
|
||||
"""Support for light effects for the LIFX light platform."""
|
||||
import logging
|
||||
import asyncio
|
||||
import random
|
||||
from os import path
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.light import (
|
||||
DOMAIN, ATTR_BRIGHTNESS, ATTR_COLOR_NAME, ATTR_RGB_COLOR, ATTR_EFFECT)
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.const import (ATTR_ENTITY_ID)
|
||||
from homeassistant.helpers.service import extract_entity_ids
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SERVICE_EFFECT_BREATHE = 'lifx_effect_breathe'
|
||||
SERVICE_EFFECT_PULSE = 'lifx_effect_pulse'
|
||||
SERVICE_EFFECT_COLORLOOP = 'lifx_effect_colorloop'
|
||||
SERVICE_EFFECT_STOP = 'lifx_effect_stop'
|
||||
|
||||
ATTR_POWER_ON = 'power_on'
|
||||
ATTR_PERIOD = 'period'
|
||||
ATTR_CYCLES = 'cycles'
|
||||
ATTR_SPREAD = 'spread'
|
||||
ATTR_CHANGE = 'change'
|
||||
|
||||
# aiolifx waveform modes
|
||||
WAVEFORM_SINE = 1
|
||||
WAVEFORM_PULSE = 4
|
||||
|
||||
LIFX_EFFECT_SCHEMA = vol.Schema({
|
||||
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
||||
vol.Optional(ATTR_POWER_ON, default=True): cv.boolean,
|
||||
})
|
||||
|
||||
LIFX_EFFECT_BREATHE_SCHEMA = LIFX_EFFECT_SCHEMA.extend({
|
||||
ATTR_BRIGHTNESS: vol.All(vol.Coerce(int), vol.Clamp(min=0, max=255)),
|
||||
ATTR_COLOR_NAME: cv.string,
|
||||
ATTR_RGB_COLOR: vol.All(vol.ExactSequence((cv.byte, cv.byte, cv.byte)),
|
||||
vol.Coerce(tuple)),
|
||||
vol.Optional(ATTR_PERIOD, default=1.0): vol.All(vol.Coerce(float),
|
||||
vol.Range(min=0.05)),
|
||||
vol.Optional(ATTR_CYCLES, default=1.0): vol.All(vol.Coerce(float),
|
||||
vol.Range(min=1)),
|
||||
})
|
||||
|
||||
LIFX_EFFECT_PULSE_SCHEMA = LIFX_EFFECT_BREATHE_SCHEMA
|
||||
|
||||
LIFX_EFFECT_COLORLOOP_SCHEMA = LIFX_EFFECT_SCHEMA.extend({
|
||||
ATTR_BRIGHTNESS: vol.All(vol.Coerce(int), vol.Clamp(min=0, max=255)),
|
||||
vol.Optional(ATTR_PERIOD, default=60): vol.All(vol.Coerce(float),
|
||||
vol.Clamp(min=1)),
|
||||
vol.Optional(ATTR_CHANGE, default=20): vol.All(vol.Coerce(float),
|
||||
vol.Clamp(min=0, max=360)),
|
||||
vol.Optional(ATTR_SPREAD, default=30): vol.All(vol.Coerce(float),
|
||||
vol.Clamp(min=0, max=360)),
|
||||
})
|
||||
|
||||
LIFX_EFFECT_STOP_SCHEMA = vol.Schema({
|
||||
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
||||
vol.Optional(ATTR_POWER_ON, default=False): cv.boolean,
|
||||
})
|
||||
|
||||
|
||||
def setup(hass, lifx_manager):
|
||||
"""Register the LIFX effects as hass service calls."""
|
||||
@asyncio.coroutine
|
||||
def async_service_handle(service):
|
||||
"""Internal func for applying a service."""
|
||||
entity_ids = extract_entity_ids(hass, service)
|
||||
if entity_ids:
|
||||
devices = [entity for entity in lifx_manager.entities.values()
|
||||
if entity.entity_id in entity_ids]
|
||||
else:
|
||||
devices = list(lifx_manager.entities.values())
|
||||
|
||||
if devices:
|
||||
yield from start_effect(hass, devices,
|
||||
service.service, **service.data)
|
||||
|
||||
descriptions = load_yaml_config_file(
|
||||
path.join(path.dirname(__file__), 'services.yaml'))
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_EFFECT_BREATHE, async_service_handle,
|
||||
descriptions.get(SERVICE_EFFECT_BREATHE),
|
||||
schema=LIFX_EFFECT_BREATHE_SCHEMA)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_EFFECT_PULSE, async_service_handle,
|
||||
descriptions.get(SERVICE_EFFECT_PULSE),
|
||||
schema=LIFX_EFFECT_PULSE_SCHEMA)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_EFFECT_COLORLOOP, async_service_handle,
|
||||
descriptions.get(SERVICE_EFFECT_COLORLOOP),
|
||||
schema=LIFX_EFFECT_COLORLOOP_SCHEMA)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_EFFECT_STOP, async_service_handle,
|
||||
descriptions.get(SERVICE_EFFECT_STOP),
|
||||
schema=LIFX_EFFECT_STOP_SCHEMA)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def start_effect(hass, devices, service, **data):
|
||||
"""Start a light effect."""
|
||||
tasks = []
|
||||
for light in devices:
|
||||
tasks.append(hass.async_add_job(light.stop_effect()))
|
||||
yield from asyncio.wait(tasks, loop=hass.loop)
|
||||
|
||||
if service in SERVICE_EFFECT_BREATHE:
|
||||
effect = LIFXEffectBreathe(hass, devices)
|
||||
elif service in SERVICE_EFFECT_PULSE:
|
||||
effect = LIFXEffectPulse(hass, devices)
|
||||
elif service == SERVICE_EFFECT_COLORLOOP:
|
||||
effect = LIFXEffectColorloop(hass, devices)
|
||||
elif service == SERVICE_EFFECT_STOP:
|
||||
effect = LIFXEffectStop(hass, devices)
|
||||
|
||||
hass.async_add_job(effect.async_perform(**data))
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def default_effect(light, **kwargs):
|
||||
"""Start an effect with default parameters."""
|
||||
service = kwargs[ATTR_EFFECT]
|
||||
data = {
|
||||
ATTR_ENTITY_ID: light.entity_id,
|
||||
}
|
||||
if service in (SERVICE_EFFECT_BREATHE, SERVICE_EFFECT_PULSE):
|
||||
data[ATTR_RGB_COLOR] = [
|
||||
random.randint(1, 127),
|
||||
random.randint(1, 127),
|
||||
random.randint(1, 127),
|
||||
]
|
||||
data[ATTR_BRIGHTNESS] = 255
|
||||
yield from light.hass.services.async_call(DOMAIN, service, data)
|
||||
|
||||
|
||||
def effect_list():
|
||||
"""Return the list of supported effects."""
|
||||
return [
|
||||
SERVICE_EFFECT_COLORLOOP,
|
||||
SERVICE_EFFECT_BREATHE,
|
||||
SERVICE_EFFECT_PULSE,
|
||||
SERVICE_EFFECT_STOP,
|
||||
]
|
||||
|
||||
|
||||
class LIFXEffectData(object):
|
||||
"""Structure describing a running effect."""
|
||||
|
||||
def __init__(self, effect, power, color):
|
||||
"""Initialize data structure."""
|
||||
self.effect = effect
|
||||
self.power = power
|
||||
self.color = color
|
||||
|
||||
|
||||
class LIFXEffect(object):
|
||||
"""Representation of a light effect running on a number of lights."""
|
||||
|
||||
def __init__(self, hass, lights):
|
||||
"""Initialize the effect."""
|
||||
self.hass = hass
|
||||
self.lights = lights
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_perform(self, **kwargs):
|
||||
"""Do common setup and play the effect."""
|
||||
yield from self.async_setup(**kwargs)
|
||||
yield from self.async_play(**kwargs)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(self, **kwargs):
|
||||
"""Prepare all lights for the effect."""
|
||||
for light in self.lights:
|
||||
yield from light.refresh_state()
|
||||
if not light.device:
|
||||
self.lights.remove(light)
|
||||
else:
|
||||
light.effect_data = LIFXEffectData(
|
||||
self, light.is_on, light.device.color)
|
||||
|
||||
# Temporarily turn on power for the effect to be visible
|
||||
if kwargs[ATTR_POWER_ON] and not light.is_on:
|
||||
hsbk = self.from_poweroff_hsbk(light, **kwargs)
|
||||
light.device.set_color(hsbk)
|
||||
light.device.set_power(True)
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
@asyncio.coroutine
|
||||
def async_play(self, **kwargs):
|
||||
"""Play the effect."""
|
||||
yield None
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_restore(self, light):
|
||||
"""Restore to the original state (if we are still running)."""
|
||||
if light.effect_data:
|
||||
if light.effect_data.effect == self:
|
||||
if light.device and not light.effect_data.power:
|
||||
light.device.set_power(False)
|
||||
yield from asyncio.sleep(0.5)
|
||||
if light.device:
|
||||
light.device.set_color(light.effect_data.color)
|
||||
yield from asyncio.sleep(0.5)
|
||||
light.effect_data = None
|
||||
self.lights.remove(light)
|
||||
|
||||
def from_poweroff_hsbk(self, light, **kwargs):
|
||||
"""The initial color when starting from a powered off state."""
|
||||
return None
|
||||
|
||||
|
||||
class LIFXEffectBreathe(LIFXEffect):
|
||||
"""Representation of a breathe effect."""
|
||||
|
||||
def __init__(self, hass, lights):
|
||||
"""Initialize the breathe effect."""
|
||||
super(LIFXEffectBreathe, self).__init__(hass, lights)
|
||||
self.name = SERVICE_EFFECT_BREATHE
|
||||
self.waveform = WAVEFORM_SINE
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_play(self, **kwargs):
|
||||
"""Play the effect on all lights."""
|
||||
for light in self.lights:
|
||||
self.hass.async_add_job(self.async_light_play(light, **kwargs))
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_light_play(self, light, **kwargs):
|
||||
"""Play a light effect on the bulb."""
|
||||
period = kwargs[ATTR_PERIOD]
|
||||
cycles = kwargs[ATTR_CYCLES]
|
||||
hsbk, _ = light.find_hsbk(**kwargs)
|
||||
|
||||
# Start the effect
|
||||
args = {
|
||||
'transient': 1,
|
||||
'color': hsbk,
|
||||
'period': int(period*1000),
|
||||
'cycles': cycles,
|
||||
'duty_cycle': 0,
|
||||
'waveform': self.waveform,
|
||||
}
|
||||
light.device.set_waveform(args)
|
||||
|
||||
# Wait for completion and restore the initial state
|
||||
yield from asyncio.sleep(period*cycles)
|
||||
yield from self.async_restore(light)
|
||||
|
||||
def from_poweroff_hsbk(self, light, **kwargs):
|
||||
"""Initial color is the target color, but no brightness."""
|
||||
hsbk, _ = light.find_hsbk(**kwargs)
|
||||
return [hsbk[0], hsbk[1], 0, hsbk[2]]
|
||||
|
||||
|
||||
class LIFXEffectPulse(LIFXEffectBreathe):
|
||||
"""Representation of a pulse effect."""
|
||||
|
||||
def __init__(self, hass, lights):
|
||||
"""Initialize the pulse effect."""
|
||||
super(LIFXEffectPulse, self).__init__(hass, lights)
|
||||
self.name = SERVICE_EFFECT_PULSE
|
||||
self.waveform = WAVEFORM_PULSE
|
||||
|
||||
|
||||
class LIFXEffectColorloop(LIFXEffect):
|
||||
"""Representation of a colorloop effect."""
|
||||
|
||||
def __init__(self, hass, lights):
|
||||
"""Initialize the colorloop effect."""
|
||||
super(LIFXEffectColorloop, self).__init__(hass, lights)
|
||||
self.name = SERVICE_EFFECT_COLORLOOP
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_play(self, **kwargs):
|
||||
"""Play the effect on all lights."""
|
||||
period = kwargs[ATTR_PERIOD]
|
||||
spread = kwargs[ATTR_SPREAD]
|
||||
change = kwargs[ATTR_CHANGE]
|
||||
direction = 1 if random.randint(0, 1) else -1
|
||||
|
||||
# Random start
|
||||
hue = random.randint(0, 359)
|
||||
|
||||
while self.lights:
|
||||
hue = (hue + direction*change) % 360
|
||||
|
||||
random.shuffle(self.lights)
|
||||
lhue = hue
|
||||
|
||||
transition = int(1000 * random.uniform(period/2, period))
|
||||
for light in self.lights:
|
||||
if spread > 0:
|
||||
transition = int(1000 * random.uniform(period/2, period))
|
||||
|
||||
if ATTR_BRIGHTNESS in kwargs:
|
||||
brightness = int(65535/255*kwargs[ATTR_BRIGHTNESS])
|
||||
else:
|
||||
brightness = light.effect_data.color[2]
|
||||
|
||||
hsbk = [
|
||||
int(65535/359*lhue),
|
||||
int(random.uniform(0.8, 1.0)*65535),
|
||||
brightness,
|
||||
4000,
|
||||
]
|
||||
light.device.set_color(hsbk, None, transition)
|
||||
|
||||
# Adjust the next light so the full spread is used
|
||||
if len(self.lights) > 1:
|
||||
lhue = (lhue + spread/(len(self.lights)-1)) % 360
|
||||
|
||||
yield from asyncio.sleep(period)
|
||||
|
||||
def from_poweroff_hsbk(self, light, **kwargs):
|
||||
"""Start from a random hue."""
|
||||
return [random.randint(0, 65535), 65535, 0, 4000]
|
||||
|
||||
|
||||
class LIFXEffectStop(LIFXEffect):
|
||||
"""A no-op effect, but starting it will stop an existing effect."""
|
||||
|
||||
def __init__(self, hass, lights):
|
||||
"""Initialize the stop effect."""
|
||||
super(LIFXEffectStop, self).__init__(hass, lights)
|
||||
self.name = SERVICE_EFFECT_STOP
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_perform(self, **kwargs):
|
||||
"""Do nothing."""
|
||||
yield None
|
99
homeassistant/components/light/lifx/services.yaml
Normal file
99
homeassistant/components/light/lifx/services.yaml
Normal file
@ -0,0 +1,99 @@
|
||||
lifx_effect_breathe:
|
||||
description: Run a breathe effect by fading to a color and back.
|
||||
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of entities to run the effect on
|
||||
example: 'light.kitchen'
|
||||
|
||||
brightness:
|
||||
description: Number between 0..255 indicating brightness when the effect peaks
|
||||
example: 120
|
||||
|
||||
color_name:
|
||||
description: A human readable color name
|
||||
example: 'red'
|
||||
|
||||
rgb_color:
|
||||
description: Color for the fade in RGB-format
|
||||
example: '[255, 100, 100]'
|
||||
|
||||
period:
|
||||
description: Duration of the effect in seconds (default 1.0)
|
||||
example: 3
|
||||
|
||||
cycles:
|
||||
description: Number of times the effect should run (default 1.0)
|
||||
example: 2
|
||||
|
||||
power_on:
|
||||
description: Powered off lights are temporarily turned on during the effect (default True)
|
||||
example: False
|
||||
|
||||
lifx_effect_pulse:
|
||||
description: Run a flash effect by changing to a color and back.
|
||||
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of entities to run the effect on
|
||||
example: 'light.kitchen'
|
||||
|
||||
brightness:
|
||||
description: Number between 0..255 indicating brightness of the temporary color
|
||||
example: 120
|
||||
|
||||
color_name:
|
||||
description: A human readable color name
|
||||
example: 'red'
|
||||
|
||||
rgb_color:
|
||||
description: The temporary color in RGB-format
|
||||
example: '[255, 100, 100]'
|
||||
|
||||
period:
|
||||
description: Duration of the effect in seconds (default 1.0)
|
||||
example: 3
|
||||
|
||||
cycles:
|
||||
description: Number of times the effect should run (default 1.0)
|
||||
example: 2
|
||||
|
||||
power_on:
|
||||
description: Powered off lights are temporarily turned on during the effect (default True)
|
||||
example: False
|
||||
|
||||
lifx_effect_colorloop:
|
||||
description: Run an effect with looping colors.
|
||||
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of entities to run the effect on
|
||||
example: 'light.disco1, light.disco2, light.disco3'
|
||||
|
||||
brightness:
|
||||
description: Number between 0..255 indicating brightness of the effect. Leave this out to maintain the current brightness of each participating light
|
||||
example: 120
|
||||
|
||||
period:
|
||||
description: Duration between color changes (deafult 60)
|
||||
example: 180
|
||||
|
||||
change:
|
||||
description: Hue movement per period, in degrees on a color wheel (default 20)
|
||||
example: 45
|
||||
|
||||
spread:
|
||||
description: Maximum hue difference between participating lights, in degrees on a color wheel (default 30)
|
||||
example: 0
|
||||
|
||||
power_on:
|
||||
description: Powered off lights are temporarily turned on during the effect (default True)
|
||||
example: False
|
||||
|
||||
lifx_effect_stop:
|
||||
description: Stop a running effect.
|
||||
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of entities to stop effects on. Leave out to stop effects everywhere.
|
||||
example: 'light.bedroom'
|
@ -14,12 +14,9 @@ _LOGGER = logging.getLogger(__name__)
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup Lutron lights."""
|
||||
area_devs = {}
|
||||
devs = []
|
||||
for (area_name, device) in hass.data[LUTRON_DEVICES]['light']:
|
||||
dev = LutronLight(hass, area_name, device,
|
||||
hass.data[LUTRON_CONTROLLER])
|
||||
area_devs.setdefault(area_name, []).append(dev)
|
||||
dev = LutronLight(area_name, device, hass.data[LUTRON_CONTROLLER])
|
||||
devs.append(dev)
|
||||
|
||||
add_devices(devs, True)
|
||||
@ -39,10 +36,10 @@ def to_hass_level(level):
|
||||
class LutronLight(LutronDevice, Light):
|
||||
"""Representation of a Lutron Light, including dimmable."""
|
||||
|
||||
def __init__(self, hass, area_name, lutron_device, controller):
|
||||
def __init__(self, area_name, lutron_device, controller):
|
||||
"""Initialize the light."""
|
||||
self._prev_brightness = None
|
||||
LutronDevice.__init__(self, hass, area_name, lutron_device, controller)
|
||||
LutronDevice.__init__(self, area_name, lutron_device, controller)
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
|
@ -42,7 +42,7 @@ class LutronCasetaLight(LutronCasetaDevice, Light):
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
"""Turn the light on."""
|
||||
if ATTR_BRIGHTNESS in kwargs and self._device_type == "WallDimmer":
|
||||
if ATTR_BRIGHTNESS in kwargs:
|
||||
brightness = kwargs[ATTR_BRIGHTNESS]
|
||||
else:
|
||||
brightness = 255
|
||||
|
121
homeassistant/components/light/mystrom.py
Normal file
121
homeassistant/components/light/mystrom.py
Normal file
@ -0,0 +1,121 @@
|
||||
"""
|
||||
Support for myStrom Wifi bulbs.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/light.mystrom/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.light import (
|
||||
Light, PLATFORM_SCHEMA, ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS)
|
||||
from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, STATE_UNKNOWN
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['python-mystrom==0.3.8']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_NAME = 'myStrom bulb'
|
||||
|
||||
SUPPORT_MYSTROM = (SUPPORT_BRIGHTNESS)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Required(CONF_MAC): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the myStrom Light platform."""
|
||||
from pymystrom import MyStromBulb
|
||||
from pymystrom.exceptions import MyStromConnectionError
|
||||
|
||||
host = config.get(CONF_HOST)
|
||||
mac = config.get(CONF_MAC)
|
||||
name = config.get(CONF_NAME)
|
||||
|
||||
bulb = MyStromBulb(host, mac)
|
||||
try:
|
||||
if bulb.get_status()['type'] != 'rgblamp':
|
||||
_LOGGER.error("Device %s (%s) is not a myStrom bulb", host, mac)
|
||||
return False
|
||||
except MyStromConnectionError:
|
||||
_LOGGER.warning("myStrom bulb not online")
|
||||
|
||||
add_devices([MyStromLight(bulb, name)], True)
|
||||
|
||||
|
||||
class MyStromLight(Light):
|
||||
"""Representation of the myStrom WiFi Bulb."""
|
||||
|
||||
def __init__(self, bulb, name):
|
||||
"""Initialize the light."""
|
||||
self._bulb = bulb
|
||||
self._name = name
|
||||
self._state = None
|
||||
self._available = False
|
||||
self._brightness = 0
|
||||
self._rgb_color = [0, 0, 0]
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the display name of this light."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Flag supported features."""
|
||||
return SUPPORT_MYSTROM
|
||||
|
||||
@property
|
||||
def brightness(self):
|
||||
"""Brightness of the light."""
|
||||
return self._brightness
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return True if entity is available."""
|
||||
return self._available
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if light is on."""
|
||||
return self._state['on'] if self._state is not None else STATE_UNKNOWN
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
"""Turn on the light."""
|
||||
from pymystrom.exceptions import MyStromConnectionError
|
||||
|
||||
brightness = kwargs.get(ATTR_BRIGHTNESS, 255)
|
||||
|
||||
try:
|
||||
if not self.is_on:
|
||||
self._bulb.set_on()
|
||||
if brightness is not None:
|
||||
self._bulb.set_color_hsv(0, 0, round(brightness * 100 / 255))
|
||||
except MyStromConnectionError:
|
||||
_LOGGER.warning("myStrom bulb not online")
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
"""Turn off the bulb."""
|
||||
from pymystrom.exceptions import MyStromConnectionError
|
||||
|
||||
try:
|
||||
self._bulb.set_off()
|
||||
except MyStromConnectionError:
|
||||
_LOGGER.warning("myStrom bulb not online")
|
||||
|
||||
def update(self):
|
||||
"""Fetch new state data for this light."""
|
||||
from pymystrom.exceptions import MyStromConnectionError
|
||||
|
||||
try:
|
||||
self._state = self._bulb.get_status()
|
||||
self._brightness = int(self._bulb.get_brightness()) * 255 / 100
|
||||
self._available = True
|
||||
except MyStromConnectionError:
|
||||
_LOGGER.warning("myStrom bulb not online")
|
||||
self._available = False
|
213
homeassistant/components/light/rpi_gpio_pwm.py
Normal file
213
homeassistant/components/light/rpi_gpio_pwm.py
Normal file
@ -0,0 +1,213 @@
|
||||
"""
|
||||
Support for LED lights that can be controlled using PWM.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/light.pwm/
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import CONF_NAME, CONF_TYPE
|
||||
from homeassistant.components.light import (
|
||||
Light, ATTR_BRIGHTNESS, ATTR_RGB_COLOR, ATTR_TRANSITION,
|
||||
SUPPORT_BRIGHTNESS, SUPPORT_RGB_COLOR, SUPPORT_TRANSITION, PLATFORM_SCHEMA)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['pwmled==1.1.1']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_LEDS = 'leds'
|
||||
CONF_DRIVER = 'driver'
|
||||
CONF_PINS = 'pins'
|
||||
CONF_FREQUENCY = 'frequency'
|
||||
CONF_ADDRESS = 'address'
|
||||
|
||||
CONF_DRIVER_GPIO = 'gpio'
|
||||
CONF_DRIVER_PCA9685 = 'pca9685'
|
||||
CONF_DRIVER_TYPES = [CONF_DRIVER_GPIO, CONF_DRIVER_PCA9685]
|
||||
|
||||
CONF_LED_TYPE_SIMPLE = 'simple'
|
||||
CONF_LED_TYPE_RGB = 'rgb'
|
||||
CONF_LED_TYPE_RGBW = 'rgbw'
|
||||
CONF_LED_TYPES = [CONF_LED_TYPE_SIMPLE, CONF_LED_TYPE_RGB, CONF_LED_TYPE_RGBW]
|
||||
|
||||
DEFAULT_COLOR = [255, 255, 255]
|
||||
|
||||
SUPPORT_SIMPLE_LED = (SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION)
|
||||
SUPPORT_RGB_LED = (SUPPORT_BRIGHTNESS | SUPPORT_RGB_COLOR | SUPPORT_TRANSITION)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_LEDS): vol.All(cv.ensure_list, [
|
||||
{
|
||||
vol.Required(CONF_NAME): cv.string,
|
||||
vol.Required(CONF_DRIVER): vol.In(CONF_DRIVER_TYPES),
|
||||
vol.Required(CONF_PINS): vol.All(cv.ensure_list,
|
||||
[cv.positive_int]),
|
||||
vol.Required(CONF_TYPE): vol.In(CONF_LED_TYPES),
|
||||
vol.Optional(CONF_FREQUENCY): cv.positive_int,
|
||||
vol.Optional(CONF_ADDRESS): cv.byte
|
||||
}
|
||||
])
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the pwm lights."""
|
||||
from pwmled.led import SimpleLed
|
||||
from pwmled.led.rgb import RgbLed
|
||||
from pwmled.led.rgbw import RgbwLed
|
||||
from pwmled.driver.gpio import GpioDriver
|
||||
from pwmled.driver.pca9685 import Pca9685Driver
|
||||
|
||||
leds = []
|
||||
for led_conf in config[CONF_LEDS]:
|
||||
driver_type = led_conf[CONF_DRIVER]
|
||||
pins = led_conf[CONF_PINS]
|
||||
opt_args = {}
|
||||
if CONF_FREQUENCY in led_conf:
|
||||
opt_args['freq'] = led_conf[CONF_FREQUENCY]
|
||||
if driver_type == CONF_DRIVER_GPIO:
|
||||
driver = GpioDriver(pins, **opt_args)
|
||||
elif driver_type == CONF_DRIVER_PCA9685:
|
||||
if CONF_ADDRESS in led_conf:
|
||||
opt_args['address'] = led_conf[CONF_ADDRESS]
|
||||
driver = Pca9685Driver(pins, **opt_args)
|
||||
else:
|
||||
_LOGGER.error("Invalid driver type.")
|
||||
return
|
||||
|
||||
name = led_conf[CONF_NAME]
|
||||
led_type = led_conf[CONF_TYPE]
|
||||
if led_type == CONF_LED_TYPE_SIMPLE:
|
||||
led = PwmSimpleLed(SimpleLed(driver), name)
|
||||
elif led_type == CONF_LED_TYPE_RGB:
|
||||
led = PwmRgbLed(RgbLed(driver), name)
|
||||
elif led_type == CONF_LED_TYPE_RGBW:
|
||||
led = PwmRgbLed(RgbwLed(driver), name)
|
||||
else:
|
||||
_LOGGER.error("Invalid led type.")
|
||||
return
|
||||
leds.append(led)
|
||||
|
||||
add_devices(leds)
|
||||
|
||||
|
||||
class PwmSimpleLed(Light):
|
||||
"""Representation of a simple on-color pwm led."""
|
||||
|
||||
def __init__(self, led, name):
|
||||
"""Initialize led."""
|
||||
self._led = led
|
||||
self._name = name
|
||||
self._is_on = False
|
||||
self._brightness = 255
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the group."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if device is on."""
|
||||
return self._is_on
|
||||
|
||||
@property
|
||||
def brightness(self):
|
||||
"""Return the brightness property."""
|
||||
return self._brightness
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Flag supported features."""
|
||||
return SUPPORT_SIMPLE_LED
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
"""Turn on a led."""
|
||||
if ATTR_BRIGHTNESS in kwargs:
|
||||
self._brightness = kwargs[ATTR_BRIGHTNESS]
|
||||
|
||||
if ATTR_TRANSITION in kwargs:
|
||||
transition_time = kwargs[ATTR_TRANSITION]
|
||||
self._led.transition(
|
||||
transition_time,
|
||||
is_on=True,
|
||||
brightness=_from_hass_brightness(self._brightness))
|
||||
else:
|
||||
self._led.set(is_on=True,
|
||||
brightness=_from_hass_brightness(self._brightness))
|
||||
|
||||
self._is_on = True
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
"""Turn off a led."""
|
||||
if self.is_on:
|
||||
if ATTR_TRANSITION in kwargs:
|
||||
transition_time = kwargs[ATTR_TRANSITION]
|
||||
self._led.transition(transition_time, is_on=False)
|
||||
else:
|
||||
self._led.off()
|
||||
|
||||
self._is_on = False
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
|
||||
class PwmRgbLed(PwmSimpleLed):
|
||||
"""Representation of a rgb(w) pwm led."""
|
||||
|
||||
def __init__(self, led, name):
|
||||
"""Initialize led."""
|
||||
super().__init__(led, name)
|
||||
self._color = DEFAULT_COLOR
|
||||
|
||||
@property
|
||||
def rgb_color(self):
|
||||
"""Return the color property."""
|
||||
return self._color
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Flag supported features."""
|
||||
return SUPPORT_RGB_LED
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
"""Turn on a led."""
|
||||
if ATTR_RGB_COLOR in kwargs:
|
||||
self._color = kwargs[ATTR_RGB_COLOR]
|
||||
if ATTR_BRIGHTNESS in kwargs:
|
||||
self._brightness = kwargs[ATTR_BRIGHTNESS]
|
||||
|
||||
if ATTR_TRANSITION in kwargs:
|
||||
transition_time = kwargs[ATTR_TRANSITION]
|
||||
self._led.transition(
|
||||
transition_time,
|
||||
is_on=True,
|
||||
brightness=_from_hass_brightness(self._brightness),
|
||||
color=_from_hass_color(self._color))
|
||||
else:
|
||||
self._led.set(is_on=True,
|
||||
brightness=_from_hass_brightness(self._brightness),
|
||||
color=_from_hass_color(self._color))
|
||||
|
||||
self._is_on = True
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
|
||||
def _from_hass_brightness(brightness):
|
||||
"""Convert Home Assistant brightness units to percentage."""
|
||||
return brightness / 255
|
||||
|
||||
|
||||
def _from_hass_color(color):
|
||||
"""Convert Home Assistant RGB list to Color tuple."""
|
||||
from pwmled import Color
|
||||
return Color(*tuple(color))
|
135
homeassistant/components/light/tradfri.py
Normal file
135
homeassistant/components/light/tradfri.py
Normal file
@ -0,0 +1,135 @@
|
||||
"""Support for the IKEA Tradfri platform."""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_RGB_COLOR, SUPPORT_BRIGHTNESS,
|
||||
SUPPORT_COLOR_TEMP, SUPPORT_RGB_COLOR, Light)
|
||||
from homeassistant.components.light import \
|
||||
PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA
|
||||
from homeassistant.components.tradfri import KEY_GATEWAY
|
||||
from homeassistant.util import color as color_util
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['tradfri']
|
||||
PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA
|
||||
IKEA = 'IKEA of Sweden'
|
||||
ALLOWED_TEMPERATURES = {
|
||||
IKEA: {2200: 'efd275', 2700: 'f1e0b5', 4000: 'f5faf6'}
|
||||
}
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the IKEA Tradfri Light platform."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
gateway_id = discovery_info['gateway']
|
||||
gateway = hass.data[KEY_GATEWAY][gateway_id]
|
||||
devices = gateway.get_devices()
|
||||
lights = [dev for dev in devices if dev.has_light_control]
|
||||
add_devices(Tradfri(light) for light in lights)
|
||||
|
||||
|
||||
class Tradfri(Light):
|
||||
"""The platform class required by hass."""
|
||||
|
||||
def __init__(self, light):
|
||||
"""Initialize a Light."""
|
||||
self._light = light
|
||||
|
||||
# Caching of LightControl and light object
|
||||
self._light_control = light.light_control
|
||||
self._light_data = light.light_control.lights[0]
|
||||
self._name = light.name
|
||||
self._rgb_color = None
|
||||
self._features = SUPPORT_BRIGHTNESS
|
||||
|
||||
if self._light_data.hex_color is not None:
|
||||
if self._light.device_info.manufacturer == IKEA:
|
||||
self._features |= SUPPORT_COLOR_TEMP
|
||||
else:
|
||||
self._features |= SUPPORT_RGB_COLOR
|
||||
|
||||
self._ok_temps = ALLOWED_TEMPERATURES.get(
|
||||
self._light.device_info.manufacturer)
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Flag supported features."""
|
||||
return self._features
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the display name of this light."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if light is on."""
|
||||
return self._light_data.state
|
||||
|
||||
@property
|
||||
def brightness(self):
|
||||
"""Brightness of the light (an integer in the range 1-255)."""
|
||||
return self._light_data.dimmer
|
||||
|
||||
@property
|
||||
def color_temp(self):
|
||||
"""Return the CT color value in mireds."""
|
||||
if (self._light_data.hex_color is None or
|
||||
self.supported_features & SUPPORT_COLOR_TEMP == 0 or
|
||||
not self._ok_temps):
|
||||
return None
|
||||
|
||||
kelvin = next((
|
||||
kelvin for kelvin, hex_color in self._ok_temps.items()
|
||||
if hex_color == self._light_data.hex_color), None)
|
||||
if kelvin is None:
|
||||
_LOGGER.error(
|
||||
'unexpected color temperature found for %s: %s',
|
||||
self.name, self._light_data.hex_color)
|
||||
return
|
||||
return color_util.color_temperature_kelvin_to_mired(kelvin)
|
||||
|
||||
@property
|
||||
def rgb_color(self):
|
||||
"""RGB color of the light."""
|
||||
return self._rgb_color
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
"""Instruct the light to turn off."""
|
||||
return self._light_control.set_state(False)
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
"""
|
||||
Instruct the light to turn on.
|
||||
|
||||
After adding "self._light_data.hexcolor is not None"
|
||||
for ATTR_RGB_COLOR, this also supports Philips Hue bulbs.
|
||||
"""
|
||||
if ATTR_BRIGHTNESS in kwargs:
|
||||
self._light_control.set_dimmer(kwargs[ATTR_BRIGHTNESS])
|
||||
else:
|
||||
self._light_control.set_state(True)
|
||||
|
||||
if ATTR_RGB_COLOR in kwargs and self._light_data.hex_color is not None:
|
||||
self._light.light_control.set_hex_color(
|
||||
color_util.color_rgb_to_hex(*kwargs[ATTR_RGB_COLOR]))
|
||||
|
||||
elif ATTR_COLOR_TEMP in kwargs and \
|
||||
self._light_data.hex_color is not None and self._ok_temps:
|
||||
kelvin = color_util.color_temperature_mired_to_kelvin(
|
||||
kwargs[ATTR_COLOR_TEMP])
|
||||
# find closest allowed kelvin temp from user input
|
||||
kelvin = min(self._ok_temps.keys(), key=lambda x: abs(x - kelvin))
|
||||
self._light_control.set_hex_color(self._ok_temps[kelvin])
|
||||
|
||||
def update(self):
|
||||
"""Fetch new state data for this light."""
|
||||
self._light.update()
|
||||
|
||||
# Handle Hue lights paired with the gatway
|
||||
if self._light_data.hex_color is not None:
|
||||
self._rgb_color = color_util.rgb_hex_to_rgb_list(
|
||||
self._light_data.hex_color)
|
@ -30,8 +30,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
import pywemo.discovery as discovery
|
||||
|
||||
if discovery_info is not None:
|
||||
location = discovery_info[2]
|
||||
mac = discovery_info[3]
|
||||
location = discovery_info['ssdp_description']
|
||||
mac = discovery_info['mac_address']
|
||||
device = discovery.device_from_description(location, mac)
|
||||
|
||||
if device:
|
||||
|
@ -128,11 +128,12 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
|
||||
descriptions = load_yaml_config_file(
|
||||
path.join(path.dirname(__file__), 'services.yaml'))
|
||||
network = hass.data[zwave.ZWAVE_NETWORK]
|
||||
|
||||
def set_usercode(service):
|
||||
"""Set the usercode to index X on the lock."""
|
||||
node_id = service.data.get(zwave.const.ATTR_NODE_ID)
|
||||
lock_node = zwave.NETWORK.nodes[node_id]
|
||||
lock_node = network.nodes[node_id]
|
||||
code_slot = service.data.get(ATTR_CODE_SLOT)
|
||||
usercode = service.data.get(ATTR_USERCODE)
|
||||
|
||||
@ -151,7 +152,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
def get_usercode(service):
|
||||
"""Get a usercode at index X on the lock."""
|
||||
node_id = service.data.get(zwave.const.ATTR_NODE_ID)
|
||||
lock_node = zwave.NETWORK.nodes[node_id]
|
||||
lock_node = network.nodes[node_id]
|
||||
code_slot = service.data.get(ATTR_CODE_SLOT)
|
||||
|
||||
for value in lock_node.get_values(
|
||||
@ -164,7 +165,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
def clear_usercode(service):
|
||||
"""Set usercode to slot X on the lock."""
|
||||
node_id = service.data.get(zwave.const.ATTR_NODE_ID)
|
||||
lock_node = zwave.NETWORK.nodes[node_id]
|
||||
lock_node = network.nodes[node_id]
|
||||
code_slot = service.data.get(ATTR_CODE_SLOT)
|
||||
data = ''
|
||||
|
||||
|
@ -4,6 +4,7 @@ Component for interacting with a Lutron RadioRA 2 system.
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/lutron/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from homeassistant.helpers import discovery
|
||||
@ -50,16 +51,19 @@ def setup(hass, base_config):
|
||||
class LutronDevice(Entity):
|
||||
"""Representation of a Lutron device entity."""
|
||||
|
||||
def __init__(self, hass, area_name, lutron_device, controller):
|
||||
def __init__(self, area_name, lutron_device, controller):
|
||||
"""Initialize the device."""
|
||||
self._lutron_device = lutron_device
|
||||
self._controller = controller
|
||||
self._area_name = area_name
|
||||
|
||||
self.hass = hass
|
||||
self.object_id = '{} {}'.format(area_name, lutron_device.name)
|
||||
|
||||
self._controller.subscribe(self._lutron_device, self._update_callback)
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
self.hass.async_add_job(
|
||||
self._controller.subscribe, self._lutron_device,
|
||||
self._update_callback
|
||||
)
|
||||
|
||||
def _update_callback(self, _device):
|
||||
"""Callback invoked by pylutron when the device state changes."""
|
||||
@ -68,7 +72,7 @@ class LutronDevice(Entity):
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
return self._lutron_device.name
|
||||
return "{} {}".format(self._area_name, self._lutron_device.name)
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
|
@ -10,15 +10,13 @@ import logging
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.const import (CONF_HOST,
|
||||
CONF_USERNAME,
|
||||
CONF_PASSWORD)
|
||||
from homeassistant.const import CONF_HOST
|
||||
from homeassistant.helpers import discovery
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
REQUIREMENTS = ['https://github.com/gurumitts/'
|
||||
'pylutron-caseta/archive/v0.2.5.zip#'
|
||||
'pylutron-caseta==v0.2.5']
|
||||
'pylutron-caseta/archive/v0.2.6.zip#'
|
||||
'pylutron-caseta==v0.2.6']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -28,9 +26,7 @@ DOMAIN = 'lutron_caseta'
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string
|
||||
vol.Required(CONF_HOST): cv.string
|
||||
})
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
@ -41,9 +37,7 @@ def setup(hass, base_config):
|
||||
|
||||
config = base_config.get(DOMAIN)
|
||||
hass.data[LUTRON_CASETA_SMARTBRIDGE] = Smartbridge(
|
||||
hostname=config[CONF_HOST],
|
||||
username=config[CONF_USERNAME],
|
||||
password=config[CONF_PASSWORD]
|
||||
hostname=config[CONF_HOST]
|
||||
)
|
||||
if not hass.data[LUTRON_CASETA_SMARTBRIDGE].is_connected():
|
||||
_LOGGER.error("Unable to connect to Lutron smartbridge at %s",
|
||||
@ -71,14 +65,17 @@ class LutronCasetaDevice(Entity):
|
||||
self._device_id = device["device_id"]
|
||||
self._device_type = device["type"]
|
||||
self._device_name = device["name"]
|
||||
self._device_zone = device["zone"]
|
||||
self._state = None
|
||||
self._smartbridge = bridge
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
self._smartbridge.add_subscriber(self._device_id,
|
||||
self._update_callback)
|
||||
self.hass.async_add_job(
|
||||
self._smartbridge.add_subscriber, self._device_id,
|
||||
self._update_callback
|
||||
)
|
||||
|
||||
def _update_callback(self):
|
||||
self.schedule_update_ha_state()
|
||||
@ -91,7 +88,8 @@ class LutronCasetaDevice(Entity):
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
attr = {'Lutron Integration ID': self._device_id}
|
||||
attr = {'Device ID': self._device_id,
|
||||
'Zone ID': self._device_zone}
|
||||
return attr
|
||||
|
||||
@property
|
||||
|
@ -51,7 +51,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
if discovery_info is not None:
|
||||
name = discovery_info['name']
|
||||
host = discovery_info['host']
|
||||
login_id = discovery_info['hsgid']
|
||||
login_id = discovery_info['properties']['hG']
|
||||
start_off = False
|
||||
else:
|
||||
name = config.get(CONF_NAME)
|
||||
@ -145,6 +145,8 @@ class AppleTvDevice(MediaPlayerDevice):
|
||||
@callback
|
||||
def playstatus_update(self, updater, playing):
|
||||
"""Print what is currently playing when it changes."""
|
||||
self._playing = playing
|
||||
|
||||
if self.state == STATE_IDLE:
|
||||
self._artwork_hash = None
|
||||
elif self._has_playing_media_changed(playing):
|
||||
@ -153,7 +155,6 @@ class AppleTvDevice(MediaPlayerDevice):
|
||||
self._artwork_hash = hashlib.md5(
|
||||
base.encode('utf-8')).hexdigest()
|
||||
|
||||
self._playing = playing
|
||||
self.hass.async_add_job(self.async_update_ha_state())
|
||||
|
||||
def _has_playing_media_changed(self, new_playing):
|
||||
|
@ -21,8 +21,8 @@ from homeassistant.const import (CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = [
|
||||
'https://github.com/aparraga/braviarc/archive/0.3.6.zip'
|
||||
'#braviarc==0.3.6']
|
||||
'https://github.com/aparraga/braviarc/archive/0.3.7.zip'
|
||||
'#braviarc==0.3.7']
|
||||
|
||||
BRAVIA_CONFIG_FILE = 'bravia.conf'
|
||||
|
||||
|
@ -51,11 +51,13 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
||||
hosts = []
|
||||
|
||||
if discovery_info and discovery_info in KNOWN_HOSTS:
|
||||
return
|
||||
if discovery_info:
|
||||
host = (discovery_info.get('host'), discovery_info.get('port'))
|
||||
|
||||
elif discovery_info:
|
||||
hosts = [discovery_info]
|
||||
if host in KNOWN_HOSTS:
|
||||
return
|
||||
|
||||
hosts = [host]
|
||||
|
||||
elif CONF_HOST in config:
|
||||
hosts = [(config.get(CONF_HOST), DEFAULT_PORT)]
|
||||
|
@ -64,8 +64,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
_LOGGER.info("Denon receiver at host %s initialized", host)
|
||||
# 2. option: discovery using netdisco
|
||||
if discovery_info is not None:
|
||||
host = discovery_info[0]
|
||||
name = discovery_info[1]
|
||||
host = discovery_info.get('host')
|
||||
name = discovery_info.get('name')
|
||||
# Check if host not in cache, append it and save for later starting
|
||||
if host not in cache:
|
||||
cache.add(host)
|
||||
|
@ -37,14 +37,15 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the DirecTV platform."""
|
||||
hosts = []
|
||||
|
||||
if discovery_info and discovery_info in KNOWN_HOSTS:
|
||||
return
|
||||
if discovery_info:
|
||||
host = discovery_info.get('host')
|
||||
|
||||
if host in KNOWN_HOSTS:
|
||||
return
|
||||
|
||||
if discovery_info is not None:
|
||||
hosts.append([
|
||||
'DirecTV_' + discovery_info[1],
|
||||
discovery_info[0],
|
||||
DEFAULT_PORT
|
||||
'DirecTV_' + discovery_info.get('serial', ''),
|
||||
host, DEFAULT_PORT
|
||||
])
|
||||
|
||||
elif CONF_HOST in config:
|
||||
|
@ -47,7 +47,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
||||
if discovery_info is not None:
|
||||
add_devices(
|
||||
[FSAPIDevice(discovery_info, DEFAULT_PASSWORD)],
|
||||
[FSAPIDevice(discovery_info['ssdp_description'],
|
||||
DEFAULT_PASSWORD)],
|
||||
update_before_add=True)
|
||||
return True
|
||||
|
||||
|
@ -9,9 +9,9 @@ import logging
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.media_player import (
|
||||
MEDIA_TYPE_MUSIC, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
|
||||
SUPPORT_PAUSE, SUPPORT_SEEK, SUPPORT_STOP, SUPPORT_PLAY_MEDIA,
|
||||
SUPPORT_PLAY, SUPPORT_NEXT_TRACK, PLATFORM_SCHEMA, MediaPlayerDevice)
|
||||
MEDIA_TYPE_MUSIC, SUPPORT_VOLUME_SET, SUPPORT_PAUSE,
|
||||
SUPPORT_PLAY_MEDIA, SUPPORT_PLAY, SUPPORT_NEXT_TRACK,
|
||||
PLATFORM_SCHEMA, MediaPlayerDevice)
|
||||
from homeassistant.const import (
|
||||
STATE_IDLE, CONF_NAME, EVENT_HOMEASSISTANT_STOP)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
@ -20,14 +20,13 @@ import homeassistant.helpers.config_validation as cv
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
REQUIREMENTS = ['gstreamer-player==1.0.0']
|
||||
REQUIREMENTS = ['gstreamer-player==1.1.0']
|
||||
DOMAIN = 'gstreamer'
|
||||
CONF_PIPELINE = 'pipeline'
|
||||
|
||||
|
||||
SUPPORT_GSTREAMER = SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
|
||||
SUPPORT_PLAY | SUPPORT_PAUSE | SUPPORT_SEEK | SUPPORT_STOP | \
|
||||
SUPPORT_PLAY_MEDIA | SUPPORT_SEEK | SUPPORT_NEXT_TRACK
|
||||
SUPPORT_GSTREAMER = SUPPORT_VOLUME_SET | SUPPORT_PLAY | SUPPORT_PAUSE |\
|
||||
SUPPORT_PLAY_MEDIA | SUPPORT_NEXT_TRACK
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
@ -61,7 +60,6 @@ class GstreamerDevice(MediaPlayerDevice):
|
||||
self._state = STATE_IDLE
|
||||
self._volume = None
|
||||
self._duration = None
|
||||
self._position = None
|
||||
self._uri = None
|
||||
self._title = None
|
||||
self._artist = None
|
||||
@ -72,16 +70,11 @@ class GstreamerDevice(MediaPlayerDevice):
|
||||
self._state = self._player.state
|
||||
self._volume = self._player.volume
|
||||
self._duration = self._player.duration
|
||||
self._position = self._player.position
|
||||
self._uri = self._player.uri
|
||||
self._title = self._player.title
|
||||
self._album = self._player.album
|
||||
self._artist = self._player.artist
|
||||
|
||||
def mute_volume(self, mute):
|
||||
"""Send the mute command."""
|
||||
self._player.mute()
|
||||
|
||||
def set_volume_level(self, volume):
|
||||
"""Set the volume level."""
|
||||
self._player.volume = volume
|
||||
@ -93,9 +86,13 @@ class GstreamerDevice(MediaPlayerDevice):
|
||||
return
|
||||
self._player.queue(media_id)
|
||||
|
||||
def media_seek(self, position):
|
||||
"""Seek."""
|
||||
self._player.position = position
|
||||
def media_play(self):
|
||||
"""Play."""
|
||||
self._player.play()
|
||||
|
||||
def media_pause(self):
|
||||
"""Pause."""
|
||||
self._player.pause()
|
||||
|
||||
def media_next_track(self):
|
||||
"""Next track."""
|
||||
@ -121,11 +118,6 @@ class GstreamerDevice(MediaPlayerDevice):
|
||||
"""Return the volume level."""
|
||||
return self._volume
|
||||
|
||||
@property
|
||||
def is_volume_muted(self):
|
||||
"""Volume muted."""
|
||||
return self._volume == 0
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Flag media player features that are supported."""
|
||||
@ -141,11 +133,6 @@ class GstreamerDevice(MediaPlayerDevice):
|
||||
"""Duration of current playing media in seconds."""
|
||||
return self._duration
|
||||
|
||||
@property
|
||||
def media_position(self):
|
||||
"""Position of current playing media in seconds."""
|
||||
return self._position
|
||||
|
||||
@property
|
||||
def media_title(self):
|
||||
"""Media title."""
|
||||
|
@ -8,6 +8,7 @@ import asyncio
|
||||
from functools import wraps
|
||||
import logging
|
||||
import urllib
|
||||
import re
|
||||
|
||||
import aiohttp
|
||||
import voluptuous as vol
|
||||
@ -17,7 +18,7 @@ from homeassistant.components.media_player import (
|
||||
SUPPORT_PLAY_MEDIA, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_STOP,
|
||||
SUPPORT_TURN_OFF, SUPPORT_PLAY, SUPPORT_VOLUME_STEP, MediaPlayerDevice,
|
||||
PLATFORM_SCHEMA, MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO,
|
||||
MEDIA_TYPE_PLAYLIST)
|
||||
MEDIA_TYPE_PLAYLIST, MEDIA_PLAYER_SCHEMA, DOMAIN)
|
||||
from homeassistant.const import (
|
||||
STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING, CONF_HOST, CONF_NAME,
|
||||
CONF_PORT, CONF_SSL, CONF_PROXY_SSL, CONF_USERNAME, CONF_PASSWORD,
|
||||
@ -76,6 +77,34 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
cv.boolean,
|
||||
})
|
||||
|
||||
SERVICE_ADD_MEDIA = 'kodi_add_to_playlist'
|
||||
SERVICE_SET_SHUFFLE = 'kodi_set_shuffle'
|
||||
|
||||
ATTR_MEDIA_TYPE = 'media_type'
|
||||
ATTR_MEDIA_NAME = 'media_name'
|
||||
ATTR_MEDIA_ARTIST_NAME = 'artist_name'
|
||||
ATTR_MEDIA_ID = 'media_id'
|
||||
|
||||
MEDIA_PLAYER_SET_SHUFFLE_SCHEMA = MEDIA_PLAYER_SCHEMA.extend({
|
||||
vol.Required('shuffle_on'): cv.boolean,
|
||||
})
|
||||
|
||||
MEDIA_PLAYER_ADD_MEDIA_SCHEMA = MEDIA_PLAYER_SCHEMA.extend({
|
||||
vol.Required(ATTR_MEDIA_TYPE): cv.string,
|
||||
vol.Optional(ATTR_MEDIA_ID): cv.string,
|
||||
vol.Optional(ATTR_MEDIA_NAME): cv.string,
|
||||
vol.Optional(ATTR_MEDIA_ARTIST_NAME): cv.string,
|
||||
})
|
||||
|
||||
SERVICE_TO_METHOD = {
|
||||
SERVICE_ADD_MEDIA: {
|
||||
'method': 'async_add_media_to_playlist',
|
||||
'schema': MEDIA_PLAYER_ADD_MEDIA_SCHEMA},
|
||||
SERVICE_SET_SHUFFLE: {
|
||||
'method': 'async_set_shuffle',
|
||||
'schema': MEDIA_PLAYER_SET_SHUFFLE_SCHEMA},
|
||||
}
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
@ -103,6 +132,33 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
|
||||
async_add_devices([entity], update_before_add=True)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_service_handler(service):
|
||||
"""Map services to methods on MediaPlayerDevice."""
|
||||
method = SERVICE_TO_METHOD.get(service.service)
|
||||
if not method:
|
||||
return
|
||||
|
||||
params = {key: value for key, value in service.data.items()
|
||||
if key != 'entity_id'}
|
||||
|
||||
yield from getattr(entity, method['method'])(**params)
|
||||
|
||||
update_tasks = []
|
||||
if entity.should_poll:
|
||||
update_coro = entity.async_update_ha_state(True)
|
||||
update_tasks.append(update_coro)
|
||||
|
||||
if update_tasks:
|
||||
yield from asyncio.wait(update_tasks, loop=hass.loop)
|
||||
|
||||
for service in SERVICE_TO_METHOD:
|
||||
schema = SERVICE_TO_METHOD[service].get(
|
||||
'schema', MEDIA_PLAYER_SCHEMA)
|
||||
hass.services.async_register(
|
||||
DOMAIN, service, async_service_handler,
|
||||
description=None, schema=schema)
|
||||
|
||||
|
||||
def cmd(func):
|
||||
"""Decorator to catch command exceptions."""
|
||||
@ -593,6 +649,139 @@ class KodiDevice(MediaPlayerDevice):
|
||||
if media_type == "CHANNEL":
|
||||
return self.server.Player.Open(
|
||||
{"item": {"channelid": int(media_id)}})
|
||||
elif media_type == "PLAYLIST":
|
||||
return self.server.Player.Open(
|
||||
{"item": {"playlistid": int(media_id)}})
|
||||
else:
|
||||
return self.server.Player.Open(
|
||||
{"item": {"file": str(media_id)}})
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_set_shuffle(self, shuffle_on):
|
||||
"""Set shuffle mode, for the first player."""
|
||||
if len(self._players) < 1:
|
||||
raise RuntimeError("Error: No active player.")
|
||||
yield from self.server.Player.SetShuffle(
|
||||
{"playerid": self._players[0]['playerid'], "shuffle": shuffle_on})
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_add_media_to_playlist(
|
||||
self, media_type, media_id=None, media_name='', artist_name=''):
|
||||
"""Add a media to default playlist (i.e. playlistid=0).
|
||||
|
||||
First the media type must be selected, then
|
||||
the media can be specified in terms of id or
|
||||
name and optionally artist name.
|
||||
All the albums of an artist can be added with
|
||||
media_name="ALL"
|
||||
"""
|
||||
if media_type == "SONG":
|
||||
if media_id is None:
|
||||
media_id = yield from self.async_find_song(
|
||||
media_name, artist_name)
|
||||
|
||||
yield from self.server.Playlist.Add(
|
||||
{"playlistid": 0, "item": {"songid": int(media_id)}})
|
||||
|
||||
elif media_type == "ALBUM":
|
||||
if media_id is None:
|
||||
if media_name == "ALL":
|
||||
yield from self.async_add_all_albums(artist_name)
|
||||
return
|
||||
|
||||
media_id = yield from self.async_find_album(
|
||||
media_name, artist_name)
|
||||
|
||||
yield from self.server.Playlist.Add(
|
||||
{"playlistid": 0, "item": {"albumid": int(media_id)}})
|
||||
else:
|
||||
raise RuntimeError("Unrecognized media type.")
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_add_all_albums(self, artist_name):
|
||||
"""Add all albums of an artist to default playlist (i.e. playlistid=0).
|
||||
|
||||
The artist is specified in terms of name.
|
||||
"""
|
||||
artist_id = yield from self.async_find_artist(artist_name)
|
||||
|
||||
albums = yield from self.async_get_albums(artist_id)
|
||||
|
||||
for alb in albums['albums']:
|
||||
yield from self.server.Playlist.Add(
|
||||
{"playlistid": 0, "item": {"albumid": int(alb['albumid'])}})
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_clear_playlist(self):
|
||||
"""Clear default playlist (i.e. playlistid=0)."""
|
||||
return self.server.Playlist.Clear({"playlistid": 0})
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_get_artists(self):
|
||||
"""Get artists list."""
|
||||
return (yield from self.server.AudioLibrary.GetArtists())
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_get_albums(self, artist_id=None):
|
||||
"""Get albums list."""
|
||||
if artist_id is None:
|
||||
return (yield from self.server.AudioLibrary.GetAlbums())
|
||||
else:
|
||||
return (yield from self.server.AudioLibrary.GetAlbums(
|
||||
{"filter": {"artistid": int(artist_id)}}))
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_find_artist(self, artist_name):
|
||||
"""Find artist by name."""
|
||||
artists = yield from self.async_get_artists()
|
||||
out = self._find(
|
||||
artist_name, [a['artist'] for a in artists['artists']])
|
||||
return artists['artists'][out[0][0]]['artistid']
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_get_songs(self, artist_id=None):
|
||||
"""Get songs list."""
|
||||
if artist_id is None:
|
||||
return (yield from self.server.AudioLibrary.GetSongs())
|
||||
else:
|
||||
return (yield from self.server.AudioLibrary.GetSongs(
|
||||
{"filter": {"artistid": int(artist_id)}}))
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_find_song(self, song_name, artist_name=''):
|
||||
"""Find song by name and optionally artist name."""
|
||||
artist_id = None
|
||||
if artist_name != '':
|
||||
artist_id = yield from self.async_find_artist(artist_name)
|
||||
|
||||
songs = yield from self.async_get_songs(artist_id)
|
||||
if songs['limits']['total'] == 0:
|
||||
return None
|
||||
|
||||
out = self._find(song_name, [a['label'] for a in songs['songs']])
|
||||
return songs['songs'][out[0][0]]['songid']
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_find_album(self, album_name, artist_name=''):
|
||||
"""Find album by name and optionally artist name."""
|
||||
artist_id = None
|
||||
if artist_name != '':
|
||||
artist_id = yield from self.async_find_artist(artist_name)
|
||||
|
||||
albums = yield from self.async_get_albums(artist_id)
|
||||
out = self._find(album_name, [a['label'] for a in albums['albums']])
|
||||
return albums['albums'][out[0][0]]['albumid']
|
||||
|
||||
@staticmethod
|
||||
def _find(key_word, words):
|
||||
key_word = key_word.split(' ')
|
||||
patt = [re.compile(
|
||||
'(^| )' + k + '( |$)', re.IGNORECASE) for k in key_word]
|
||||
|
||||
out = [[i, 0] for i in range(len(words))]
|
||||
for i in range(len(words)):
|
||||
mtc = [p.search(words[i]) for p in patt]
|
||||
rate = [m is not None for m in mtc].count(True)
|
||||
out[i][1] = rate
|
||||
|
||||
return sorted(out, key=lambda out: out[1], reverse=True)
|
||||
|
@ -31,21 +31,23 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup Openhome Platform."""
|
||||
from openhomedevice.Device import Device
|
||||
|
||||
if discovery_info:
|
||||
_LOGGER.info('Openhome device found, (%s)', discovery_info[0])
|
||||
device = Device(discovery_info[1])
|
||||
|
||||
# if device has already been discovered
|
||||
if device.Uuid() in [x.unique_id for x in DEVICES]:
|
||||
return True
|
||||
|
||||
device = OpenhomeDevice(hass, device)
|
||||
|
||||
add_devices([device], True)
|
||||
DEVICES.append(device)
|
||||
|
||||
if not discovery_info:
|
||||
return True
|
||||
|
||||
name = discovery_info.get('name')
|
||||
description = discovery_info.get('ssdp_description')
|
||||
_LOGGER.info('Openhome device found, (%s)', name)
|
||||
device = Device(description)
|
||||
|
||||
# if device has already been discovered
|
||||
if device.Uuid() in [x.unique_id for x in DEVICES]:
|
||||
return True
|
||||
|
||||
device = OpenhomeDevice(hass, device)
|
||||
|
||||
add_devices([device], True)
|
||||
DEVICES.append(device)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
|
@ -51,11 +51,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
||||
if discovery_info:
|
||||
_LOGGER.debug('%s', discovery_info)
|
||||
vals = discovery_info.split(':')
|
||||
if len(vals) > 1:
|
||||
port = vals[1]
|
||||
|
||||
host = vals[0]
|
||||
host = discovery_info.get('host')
|
||||
port = discovery_info.get('port')
|
||||
remote = RemoteControl(host, port)
|
||||
add_devices([PanasonicVieraTVDevice(mac, name, remote)])
|
||||
return True
|
||||
|
@ -39,7 +39,6 @@ from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.event import track_utc_time_change
|
||||
from homeassistant.loader import get_component
|
||||
|
||||
|
||||
REQUIREMENTS = ['plexapi==2.0.2']
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
|
||||
MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1)
|
||||
@ -102,7 +101,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
# Via discovery
|
||||
elif discovery_info is not None:
|
||||
# Parse discovery data
|
||||
host = urlparse(discovery_info[1]).netloc
|
||||
host = discovery_info.get('host')
|
||||
_LOGGER.info('Discovered PLEX server: %s', host)
|
||||
|
||||
if host in _CONFIGURING:
|
||||
@ -265,6 +264,7 @@ class PlexClient(MediaPlayerDevice):
|
||||
self._machine_identifier = None
|
||||
self._make = ''
|
||||
self._media_content_id = None
|
||||
self._media_content_rating = None
|
||||
self._media_content_type = None
|
||||
self._media_duration = None
|
||||
self._media_image_url = None
|
||||
@ -274,6 +274,7 @@ class PlexClient(MediaPlayerDevice):
|
||||
self._previous_volume_level = 1 # Used in fake muting
|
||||
self._session = None
|
||||
self._session_type = None
|
||||
self._session_username = None
|
||||
self._state = STATE_IDLE
|
||||
self._volume_level = 1 # since we can't retrieve remotely
|
||||
self._volume_muted = False # since we can't retrieve remotely
|
||||
@ -343,6 +344,8 @@ class PlexClient(MediaPlayerDevice):
|
||||
self._session.viewOffset)
|
||||
self._media_content_id = self._convert_na_to_none(
|
||||
self._session.ratingKey)
|
||||
self._media_content_rating = self._convert_na_to_none(
|
||||
self._session.contentRating)
|
||||
else:
|
||||
self._media_position = None
|
||||
self._media_content_id = None
|
||||
@ -354,6 +357,8 @@ class PlexClient(MediaPlayerDevice):
|
||||
self._session.player.machineIdentifier)
|
||||
self._name = self._convert_na_to_none(self._session.player.title)
|
||||
self._player_state = self._session.player.state
|
||||
self._session_username = self._convert_na_to_none(
|
||||
self._session.username)
|
||||
self._make = self._convert_na_to_none(self._session.player.device)
|
||||
else:
|
||||
self._is_player_available = False
|
||||
@ -786,7 +791,7 @@ class PlexClient(MediaPlayerDevice):
|
||||
src['library_name']).get(src['artist_name']).album(
|
||||
src['album_name']).get(src['track_name'])
|
||||
elif media_type == 'EPISODE':
|
||||
media = self._get_episode(
|
||||
media = self._get_tv_media(
|
||||
src['library_name'], src['show_name'],
|
||||
src['season_number'], src['episode_number'])
|
||||
elif media_type == 'PLAYLIST':
|
||||
@ -795,18 +800,31 @@ class PlexClient(MediaPlayerDevice):
|
||||
media = self.device.server.library.section(
|
||||
src['library_name']).get(src['video_name'])
|
||||
|
||||
if media:
|
||||
self._client_play_media(media, shuffle=src['shuffle'])
|
||||
import plexapi.playlist
|
||||
if (media and media_type == 'EPISODE' and
|
||||
isinstance(media, plexapi.playlist.Playlist)):
|
||||
# delete episode playlist after being loaded into a play queue
|
||||
self._client_play_media(media=media, delete=True,
|
||||
shuffle=src['shuffle'])
|
||||
elif media:
|
||||
self._client_play_media(media=media, shuffle=src['shuffle'])
|
||||
|
||||
def _get_episode(self, library_name, show_name, season_number,
|
||||
episode_number):
|
||||
"""Find TV episode and return a Plex media object."""
|
||||
def _get_tv_media(self, library_name, show_name, season_number,
|
||||
episode_number):
|
||||
"""Find TV media and return a Plex media object."""
|
||||
target_season = None
|
||||
target_episode = None
|
||||
|
||||
seasons = self.device.server.library.section(library_name).get(
|
||||
show_name).seasons()
|
||||
for season in seasons:
|
||||
show = self.device.server.library.section(library_name).get(
|
||||
show_name)
|
||||
|
||||
if not season_number:
|
||||
playlist_name = "{} - {} Episodes".format(
|
||||
self.entity_id, show_name)
|
||||
return self.device.server.createPlaylist(
|
||||
playlist_name, show.episodes())
|
||||
|
||||
for season in show.seasons():
|
||||
if int(season.seasonNumber) == int(season_number):
|
||||
target_season = season
|
||||
break
|
||||
@ -817,6 +835,12 @@ class PlexClient(MediaPlayerDevice):
|
||||
str(season_number).zfill(2),
|
||||
str(episode_number).zfill(2))
|
||||
else:
|
||||
if not episode_number:
|
||||
playlist_name = "{} - {} Season {} Episodes".format(
|
||||
self.entity_id, show_name, str(season_number))
|
||||
return self.device.server.createPlaylist(
|
||||
playlist_name, target_season.episodes())
|
||||
|
||||
for episode in target_season.episodes():
|
||||
if int(episode.index) == int(episode_number):
|
||||
target_episode = episode
|
||||
@ -830,7 +854,7 @@ class PlexClient(MediaPlayerDevice):
|
||||
|
||||
return target_episode
|
||||
|
||||
def _client_play_media(self, media, **params):
|
||||
def _client_play_media(self, media, delete=False, **params):
|
||||
"""Instruct Plex client to play a piece of media."""
|
||||
if not (self.device and
|
||||
'playback' in self._device_protocol_capabilities):
|
||||
@ -838,10 +862,16 @@ class PlexClient(MediaPlayerDevice):
|
||||
return
|
||||
|
||||
import plexapi.playqueue
|
||||
server_url = media.server.baseurl.split(':')
|
||||
playqueue = plexapi.playqueue.PlayQueue.create(self.device.server,
|
||||
media, **params)
|
||||
|
||||
# delete dynamic playlists used to build playqueue (ex. play tv season)
|
||||
if delete:
|
||||
media.delete()
|
||||
|
||||
self._local_client_control_fix()
|
||||
|
||||
server_url = self.device.server.baseurl.split(':')
|
||||
self.device.sendCommand('playback/playMedia', **dict({
|
||||
'machineIdentifier':
|
||||
self.device.server.machineIdentifier,
|
||||
@ -854,3 +884,13 @@ class PlexClient(MediaPlayerDevice):
|
||||
'containerKey':
|
||||
'/playQueues/%s?window=100&own=1' % playqueue.playQueueID,
|
||||
}, **params))
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the scene state attributes."""
|
||||
attr = {}
|
||||
attr['media_content_rating'] = self._media_content_rating
|
||||
attr['session_username'] = self._session_username
|
||||
attr['media_library_name'] = self._app_name
|
||||
|
||||
return attr
|
||||
|
@ -42,11 +42,13 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Roku platform."""
|
||||
hosts = []
|
||||
|
||||
if discovery_info and discovery_info in KNOWN_HOSTS:
|
||||
return
|
||||
if discovery_info:
|
||||
host = discovery_info[0]
|
||||
|
||||
if discovery_info is not None:
|
||||
_LOGGER.debug('Discovered Roku: %s', discovery_info[0])
|
||||
if host in KNOWN_HOSTS:
|
||||
return
|
||||
|
||||
_LOGGER.debug('Discovered Roku: %s', host)
|
||||
hosts.append(discovery_info[0])
|
||||
|
||||
elif CONF_HOST in config:
|
||||
|
@ -58,7 +58,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
mac = config.get(CONF_MAC)
|
||||
timeout = config.get(CONF_TIMEOUT)
|
||||
elif discovery_info is not None:
|
||||
tv_name, model, host = discovery_info
|
||||
tv_name = discovery_info.get('name')
|
||||
model = discovery_info.get('model_name')
|
||||
host = discovery_info.get('host')
|
||||
name = "{} ({})".format(tv_name, model)
|
||||
port = DEFAULT_PORT
|
||||
timeout = DEFAULT_TIMEOUT
|
||||
|
@ -222,30 +222,39 @@ soundtouch_play_everywhere:
|
||||
description: Play on all Bose Soundtouch devices
|
||||
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of entites that will coordinate the grouping. Platform dependent. It is a shortcut for creating a multi-room zone with all devices
|
||||
master:
|
||||
description: Name of the master entity that will coordinate the grouping. Platform dependent. It is a shortcut for creating a multi-room zone with all devices
|
||||
example: 'media_player.soundtouch_home'
|
||||
|
||||
soundtouch_create_zone:
|
||||
description: Create a multi-room zone
|
||||
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of entites that will coordinate the multi-room zone. Platform dependent.
|
||||
master:
|
||||
description: Name of the master entity that will coordinate the multi-room zone. Platform dependent.
|
||||
example: 'media_player.soundtouch_home'
|
||||
slaves:
|
||||
description: Name of slaves entities to add to the new zone
|
||||
example: 'media_player.soundtouch_bedroom'
|
||||
|
||||
soundtouch_add_zone_slave:
|
||||
description: Add a slave to a multi-room zone
|
||||
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of entites that will be added to the multi-room zone. Platform dependent.
|
||||
master:
|
||||
description: Name of the master entity that is coordinating the multi-room zone. Platform dependent.
|
||||
example: 'media_player.soundtouch_home'
|
||||
slaves:
|
||||
description: Name of slaves entities to add to the existing zone
|
||||
example: 'media_player.soundtouch_bedroom'
|
||||
|
||||
soundtouch_remove_zone_slave:
|
||||
description: Remove a slave from the multi-room zone
|
||||
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of entites that will be remove from the multi-room zone. Platform dependent.
|
||||
master:
|
||||
description: Name of the master entity that is coordinating the multi-room zone. Platform dependent.
|
||||
example: 'media_player.soundtouch_home'
|
||||
slaves:
|
||||
description: Name of slaves entities to remove from the existing zone
|
||||
example: 'media_player.soundtouch_bedroom'
|
||||
|
@ -103,7 +103,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
soco.config.EVENT_ADVERTISE_IP = advertise_addr
|
||||
|
||||
if discovery_info:
|
||||
player = soco.SoCo(discovery_info)
|
||||
player = soco.SoCo(discovery_info.get('host'))
|
||||
|
||||
# if device allready exists by config
|
||||
if player.uid in [x.unique_id for x in hass.data[DATA_SONOS]]:
|
||||
@ -292,7 +292,7 @@ class SonosDevice(MediaPlayerDevice):
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Subscribe sonos events."""
|
||||
self.hass.loop.run_in_executor(None, self._subscribe_to_player_events)
|
||||
self.hass.async_add_job(self._subscribe_to_player_events)
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
|
@ -15,7 +15,7 @@ from homeassistant.const import (CONF_HOST, CONF_NAME, STATE_OFF, CONF_PORT,
|
||||
STATE_PAUSED, STATE_PLAYING,
|
||||
STATE_UNAVAILABLE)
|
||||
|
||||
REQUIREMENTS = ['libsoundtouch==0.1.0']
|
||||
REQUIREMENTS = ['libsoundtouch==0.3.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -29,33 +29,33 @@ MAP_STATUS = {
|
||||
"PLAY_STATE": STATE_PLAYING,
|
||||
"BUFFERING_STATE": STATE_PLAYING,
|
||||
"PAUSE_STATE": STATE_PAUSED,
|
||||
"STOp_STATE": STATE_OFF
|
||||
"STOP_STATE": STATE_OFF
|
||||
}
|
||||
|
||||
DATA_SOUNDTOUCH = "soundtouch"
|
||||
|
||||
SOUNDTOUCH_PLAY_EVERYWHERE = vol.Schema({
|
||||
'master': cv.entity_id,
|
||||
vol.Required('master'): cv.entity_id
|
||||
})
|
||||
|
||||
SOUNDTOUCH_CREATE_ZONE_SCHEMA = vol.Schema({
|
||||
'master': cv.entity_id,
|
||||
'slaves': cv.entity_ids
|
||||
vol.Required('master'): cv.entity_id,
|
||||
vol.Required('slaves'): cv.entity_ids
|
||||
})
|
||||
|
||||
SOUNDTOUCH_ADD_ZONE_SCHEMA = vol.Schema({
|
||||
'master': cv.entity_id,
|
||||
'slaves': cv.entity_ids
|
||||
vol.Required('master'): cv.entity_id,
|
||||
vol.Required('slaves'): cv.entity_ids
|
||||
})
|
||||
|
||||
SOUNDTOUCH_REMOVE_ZONE_SCHEMA = vol.Schema({
|
||||
'master': cv.entity_id,
|
||||
'slaves': cv.entity_ids
|
||||
vol.Required('master'): cv.entity_id,
|
||||
vol.Required('slaves'): cv.entity_ids
|
||||
})
|
||||
|
||||
DEFAULT_NAME = 'Bose Soundtouch'
|
||||
DEFAULT_PORT = 8090
|
||||
|
||||
DEVICES = []
|
||||
|
||||
SUPPORT_SOUNDTOUCH = SUPPORT_PAUSE | SUPPORT_VOLUME_STEP | \
|
||||
SUPPORT_VOLUME_MUTE | SUPPORT_PREVIOUS_TRACK | \
|
||||
SUPPORT_NEXT_TRACK | SUPPORT_TURN_OFF | \
|
||||
@ -70,180 +70,99 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Bose Soundtouch platform."""
|
||||
name = config.get(CONF_NAME)
|
||||
if DATA_SOUNDTOUCH not in hass.data:
|
||||
hass.data[DATA_SOUNDTOUCH] = []
|
||||
|
||||
remote_config = {
|
||||
'name': 'HomeAssistant',
|
||||
'description': config.get(CONF_NAME),
|
||||
'id': 'ha.component.soundtouch',
|
||||
'port': config.get(CONF_PORT),
|
||||
'host': config.get(CONF_HOST)
|
||||
}
|
||||
if discovery_info:
|
||||
# Discovery
|
||||
host = discovery_info["host"]
|
||||
port = int(discovery_info["port"])
|
||||
|
||||
soundtouch_device = SoundTouchDevice(name, remote_config)
|
||||
DEVICES.append(soundtouch_device)
|
||||
add_devices([soundtouch_device])
|
||||
# if device already exists by config
|
||||
if host in [device.config['host'] for device in
|
||||
hass.data[DATA_SOUNDTOUCH]]:
|
||||
return
|
||||
|
||||
remote_config = {
|
||||
'id': 'ha.component.soundtouch',
|
||||
'host': host,
|
||||
'port': port
|
||||
}
|
||||
soundtouch_device = SoundTouchDevice(None, remote_config)
|
||||
hass.data[DATA_SOUNDTOUCH].append(soundtouch_device)
|
||||
add_devices([soundtouch_device])
|
||||
else:
|
||||
# Config
|
||||
name = config.get(CONF_NAME)
|
||||
remote_config = {
|
||||
'id': 'ha.component.soundtouch',
|
||||
'port': config.get(CONF_PORT),
|
||||
'host': config.get(CONF_HOST)
|
||||
}
|
||||
soundtouch_device = SoundTouchDevice(name, remote_config)
|
||||
hass.data[DATA_SOUNDTOUCH].append(soundtouch_device)
|
||||
add_devices([soundtouch_device])
|
||||
|
||||
descriptions = load_yaml_config_file(
|
||||
path.join(path.dirname(__file__), 'services.yaml'))
|
||||
|
||||
def service_handle(service):
|
||||
"""Internal func for applying a service."""
|
||||
master_device_id = service.data.get('master')
|
||||
slaves_ids = service.data.get('slaves')
|
||||
slaves = []
|
||||
if slaves_ids:
|
||||
slaves = [device for device in hass.data[DATA_SOUNDTOUCH] if
|
||||
device.entity_id in slaves_ids]
|
||||
|
||||
master = next([device for device in hass.data[DATA_SOUNDTOUCH] if
|
||||
device.entity_id == master_device_id].__iter__(), None)
|
||||
|
||||
if master is None:
|
||||
_LOGGER.warning("Unable to find master with entity_id:" + str(
|
||||
master_device_id))
|
||||
return
|
||||
|
||||
if service.service == SERVICE_PLAY_EVERYWHERE:
|
||||
slaves = [d for d in hass.data[DATA_SOUNDTOUCH] if
|
||||
d.entity_id != master_device_id]
|
||||
master.create_zone(slaves)
|
||||
elif service.service == SERVICE_CREATE_ZONE:
|
||||
master.create_zone(slaves)
|
||||
elif service.service == SERVICE_REMOVE_ZONE_SLAVE:
|
||||
master.remove_zone_slave(slaves)
|
||||
elif service.service == SERVICE_ADD_ZONE_SLAVE:
|
||||
master.add_zone_slave(slaves)
|
||||
|
||||
hass.services.register(DOMAIN, SERVICE_PLAY_EVERYWHERE,
|
||||
play_everywhere_service,
|
||||
service_handle,
|
||||
descriptions.get(SERVICE_PLAY_EVERYWHERE),
|
||||
schema=SOUNDTOUCH_PLAY_EVERYWHERE)
|
||||
hass.services.register(DOMAIN, SERVICE_CREATE_ZONE,
|
||||
create_zone_service,
|
||||
service_handle,
|
||||
descriptions.get(SERVICE_CREATE_ZONE),
|
||||
schema=SOUNDTOUCH_CREATE_ZONE_SCHEMA)
|
||||
hass.services.register(DOMAIN, SERVICE_REMOVE_ZONE_SLAVE,
|
||||
remove_zone_slave,
|
||||
service_handle,
|
||||
descriptions.get(SERVICE_REMOVE_ZONE_SLAVE),
|
||||
schema=SOUNDTOUCH_REMOVE_ZONE_SCHEMA)
|
||||
hass.services.register(DOMAIN, SERVICE_ADD_ZONE_SLAVE,
|
||||
add_zone_slave,
|
||||
service_handle,
|
||||
descriptions.get(SERVICE_ADD_ZONE_SLAVE),
|
||||
schema=SOUNDTOUCH_ADD_ZONE_SCHEMA)
|
||||
|
||||
|
||||
def play_everywhere_service(service):
|
||||
"""
|
||||
Create a zone (multi-room) and play on all devices.
|
||||
|
||||
:param service: Home Assistant service with 'master' data set
|
||||
|
||||
:Example:
|
||||
|
||||
- service: media_player.soundtouch_play_everywhere
|
||||
data:
|
||||
master: media_player.soundtouch_living_room
|
||||
|
||||
"""
|
||||
master_device_id = service.data.get('master')
|
||||
slaves = [d for d in DEVICES if d.entity_id != master_device_id]
|
||||
master = next([device for device in DEVICES if
|
||||
device.entity_id == master_device_id].__iter__(), None)
|
||||
if master is None:
|
||||
_LOGGER.warning(
|
||||
"Unable to find master with entity_id:" + str(master_device_id))
|
||||
elif not slaves:
|
||||
_LOGGER.warning("Unable to create zone without slaves")
|
||||
else:
|
||||
_LOGGER.info(
|
||||
"Creating zone with master " + str(master.device.config.name))
|
||||
master.device.create_zone([slave.device for slave in slaves])
|
||||
|
||||
|
||||
def create_zone_service(service):
|
||||
"""
|
||||
Create a zone (multi-room) on a master and play on specified slaves.
|
||||
|
||||
At least one master and one slave must be specified
|
||||
|
||||
:param service: Home Assistant service with 'master' and 'slaves' data set
|
||||
|
||||
:Example:
|
||||
|
||||
- service: media_player.soundtouch_create_zone
|
||||
data:
|
||||
master: media_player.soundtouch_living_room
|
||||
slaves:
|
||||
- media_player.soundtouch_room
|
||||
- media_player.soundtouch_kitchen
|
||||
|
||||
"""
|
||||
master_device_id = service.data.get('master')
|
||||
slaves_ids = service.data.get('slaves')
|
||||
slaves = [device for device in DEVICES if device.entity_id in slaves_ids]
|
||||
master = next([device for device in DEVICES if
|
||||
device.entity_id == master_device_id].__iter__(), None)
|
||||
if master is None:
|
||||
_LOGGER.warning(
|
||||
"Unable to find master with entity_id:" + master_device_id)
|
||||
elif not slaves:
|
||||
_LOGGER.warning("Unable to create zone without slaves")
|
||||
else:
|
||||
_LOGGER.info(
|
||||
"Creating zone with master " + str(master.device.config.name))
|
||||
master.device.create_zone([slave.device for slave in slaves])
|
||||
|
||||
|
||||
def add_zone_slave(service):
|
||||
"""
|
||||
Add slave(s) to and existing zone (multi-room).
|
||||
|
||||
Zone must already exist and slaves array can not be empty.
|
||||
|
||||
:param service: Home Assistant service with 'master' and 'slaves' data set
|
||||
|
||||
:Example:
|
||||
|
||||
- service: media_player.soundtouch_add_zone_slave
|
||||
data:
|
||||
master: media_player.soundtouch_living_room
|
||||
slaves:
|
||||
- media_player.soundtouch_room
|
||||
|
||||
"""
|
||||
master_device_id = service.data.get('master')
|
||||
slaves_ids = service.data.get('slaves')
|
||||
slaves = [device for device in DEVICES if device.entity_id in slaves_ids]
|
||||
master = next([device for device in DEVICES if
|
||||
device.entity_id == master_device_id].__iter__(), None)
|
||||
if master is None:
|
||||
_LOGGER.warning(
|
||||
"Unable to find master with entity_id:" + str(master_device_id))
|
||||
elif not slaves:
|
||||
_LOGGER.warning("Unable to find slaves to add")
|
||||
else:
|
||||
_LOGGER.info(
|
||||
"Adding slaves to zone with master " + str(
|
||||
master.device.config.name))
|
||||
master.device.add_zone_slave([slave.device for slave in slaves])
|
||||
|
||||
|
||||
def remove_zone_slave(service):
|
||||
"""
|
||||
Remove slave(s) from and existing zone (multi-room).
|
||||
|
||||
Zone must already exist and slaves array can not be empty.
|
||||
Note: If removing last slave, the zone will be deleted and you'll have to
|
||||
create a new one. You will not be able to add a new slave anymore
|
||||
|
||||
:param service: Home Assistant service with 'master' and 'slaves' data set
|
||||
|
||||
:Example:
|
||||
|
||||
- service: media_player.soundtouch_remove_zone_slave
|
||||
data:
|
||||
master: media_player.soundtouch_living_room
|
||||
slaves:
|
||||
- media_player.soundtouch_room
|
||||
|
||||
"""
|
||||
master_device_id = service.data.get('master')
|
||||
slaves_ids = service.data.get('slaves')
|
||||
slaves = [device for device in DEVICES if device.entity_id in slaves_ids]
|
||||
master = next([device for device in DEVICES if
|
||||
device.entity_id == master_device_id].__iter__(), None)
|
||||
if master is None:
|
||||
_LOGGER.warning(
|
||||
"Unable to find master with entity_id:" + master_device_id)
|
||||
elif not slaves:
|
||||
_LOGGER.warning("Unable to find slaves to remove")
|
||||
else:
|
||||
_LOGGER.info("Removing slaves from zone with master " +
|
||||
str(master.device.config.name))
|
||||
master.device.remove_zone_slave([slave.device for slave in slaves])
|
||||
|
||||
|
||||
class SoundTouchDevice(MediaPlayerDevice):
|
||||
"""Representation of a SoundTouch Bose device."""
|
||||
|
||||
def __init__(self, name, config):
|
||||
"""Create Soundtouch Entity."""
|
||||
from libsoundtouch import soundtouch_device
|
||||
self._name = name
|
||||
self._device = soundtouch_device(config['host'], config['port'])
|
||||
if name is None:
|
||||
self._name = self._device.config.name
|
||||
else:
|
||||
self._name = name
|
||||
self._status = self._device.status()
|
||||
self._volume = self._device.volume()
|
||||
self._config = config
|
||||
@ -297,7 +216,7 @@ class SoundTouchDevice(MediaPlayerDevice):
|
||||
self._status = self._device.status()
|
||||
|
||||
def turn_on(self):
|
||||
"""Turn the media player on."""
|
||||
"""Turn on media player."""
|
||||
self._device.power_on()
|
||||
self._status = self._device.status()
|
||||
|
||||
@ -392,3 +311,52 @@ class SoundTouchDevice(MediaPlayerDevice):
|
||||
self._device.select_preset(preset)
|
||||
else:
|
||||
_LOGGER.warning("Unable to find preset with id " + str(media_id))
|
||||
|
||||
def create_zone(self, slaves):
|
||||
"""
|
||||
Create a zone (multi-room) and play on selected devices.
|
||||
|
||||
:param slaves: slaves on which to play
|
||||
|
||||
"""
|
||||
if not slaves:
|
||||
_LOGGER.warning("Unable to create zone without slaves")
|
||||
else:
|
||||
_LOGGER.info(
|
||||
"Creating zone with master " + str(self.device.config.name))
|
||||
self.device.create_zone([slave.device for slave in slaves])
|
||||
|
||||
def remove_zone_slave(self, slaves):
|
||||
"""
|
||||
Remove slave(s) from and existing zone (multi-room).
|
||||
|
||||
Zone must already exist and slaves array can not be empty.
|
||||
Note: If removing last slave, the zone will be deleted and you'll have
|
||||
to create a new one. You will not be able to add a new slave anymore
|
||||
|
||||
:param slaves: slaves to remove from the zone
|
||||
|
||||
"""
|
||||
if not slaves:
|
||||
_LOGGER.warning("Unable to find slaves to remove")
|
||||
else:
|
||||
_LOGGER.info("Removing slaves from zone with master " +
|
||||
str(self.device.config.name))
|
||||
self.device.remove_zone_slave([slave.device for slave in slaves])
|
||||
|
||||
def add_zone_slave(self, slaves):
|
||||
"""
|
||||
Add slave(s) to and existing zone (multi-room).
|
||||
|
||||
Zone must already exist and slaves array can not be empty.
|
||||
|
||||
:param slaves:slaves to add
|
||||
|
||||
"""
|
||||
if not slaves:
|
||||
_LOGGER.warning("Unable to find slaves to add")
|
||||
else:
|
||||
_LOGGER.info(
|
||||
"Adding slaves to zone with master " + str(
|
||||
self.device.config.name))
|
||||
self.device.add_zone_slave([slave.device for slave in slaves])
|
||||
|
286
homeassistant/components/media_player/spotify.py
Normal file
286
homeassistant/components/media_player/spotify.py
Normal file
@ -0,0 +1,286 @@
|
||||
"""
|
||||
Support for interacting with Spotify Connect.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/media_player.spotify/
|
||||
"""
|
||||
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.loader import get_component
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
from homeassistant.components.media_player import (
|
||||
MEDIA_TYPE_MUSIC, MEDIA_TYPE_PLAYLIST, SUPPORT_VOLUME_SET,
|
||||
SUPPORT_PLAY, SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA, SUPPORT_NEXT_TRACK,
|
||||
SUPPORT_PREVIOUS_TRACK, SUPPORT_SELECT_SOURCE, PLATFORM_SCHEMA,
|
||||
MediaPlayerDevice)
|
||||
from homeassistant.const import (
|
||||
CONF_NAME, STATE_PLAYING, STATE_PAUSED, STATE_IDLE, STATE_UNKNOWN)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
|
||||
COMMIT = '544614f4b1d508201d363e84e871f86c90aa26b2'
|
||||
REQUIREMENTS = ['https://github.com/happyleavesaoc/spotipy/'
|
||||
'archive/%s.zip#spotipy==2.4.4' % COMMIT]
|
||||
|
||||
DEPENDENCIES = ['http']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SUPPORT_SPOTIFY = SUPPORT_VOLUME_SET | SUPPORT_PAUSE | SUPPORT_PLAY |\
|
||||
SUPPORT_NEXT_TRACK | SUPPORT_PREVIOUS_TRACK | SUPPORT_SELECT_SOURCE |\
|
||||
SUPPORT_PLAY_MEDIA
|
||||
|
||||
SCOPE = 'user-read-playback-state user-modify-playback-state'
|
||||
DEFAULT_CACHE_PATH = '.spotify-token-cache'
|
||||
AUTH_CALLBACK_PATH = '/api/spotify'
|
||||
AUTH_CALLBACK_NAME = 'api:spotify'
|
||||
ICON = 'mdi:spotify'
|
||||
DEFAULT_NAME = 'Spotify'
|
||||
DOMAIN = 'spotify'
|
||||
CONF_CLIENT_ID = 'client_id'
|
||||
CONF_CLIENT_SECRET = 'client_secret'
|
||||
CONF_CACHE_PATH = 'cache_path'
|
||||
CONFIGURATOR_LINK_NAME = 'Link Spotify account'
|
||||
CONFIGURATOR_SUBMIT_CAPTION = 'I authorized successfully'
|
||||
CONFIGURATOR_DESCRIPTION = 'To link your Spotify account, ' \
|
||||
'click the link, login, and authorize:'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_CLIENT_ID): cv.string,
|
||||
vol.Required(CONF_CLIENT_SECRET): cv.string,
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_CACHE_PATH): cv.string
|
||||
})
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=30)
|
||||
|
||||
|
||||
def request_configuration(hass, config, add_devices, oauth):
|
||||
"""Request Spotify authorization."""
|
||||
configurator = get_component('configurator')
|
||||
hass.data[DOMAIN] = configurator.request_config(
|
||||
hass, DEFAULT_NAME, lambda _: None,
|
||||
link_name=CONFIGURATOR_LINK_NAME,
|
||||
link_url=oauth.get_authorize_url(),
|
||||
description=CONFIGURATOR_DESCRIPTION,
|
||||
submit_caption=CONFIGURATOR_SUBMIT_CAPTION)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Spotify platform."""
|
||||
import spotipy.oauth2
|
||||
callback_url = '{}{}'.format(hass.config.api.base_url, AUTH_CALLBACK_PATH)
|
||||
cache = config.get(CONF_CACHE_PATH, hass.config.path(DEFAULT_CACHE_PATH))
|
||||
oauth = spotipy.oauth2.SpotifyOAuth(
|
||||
config.get(CONF_CLIENT_ID), config.get(CONF_CLIENT_SECRET),
|
||||
callback_url, scope=SCOPE,
|
||||
cache_path=cache)
|
||||
token_info = oauth.get_cached_token()
|
||||
if not token_info:
|
||||
_LOGGER.info('no token; requesting authorization')
|
||||
hass.http.register_view(SpotifyAuthCallbackView(
|
||||
config, add_devices, oauth))
|
||||
request_configuration(hass, config, add_devices, oauth)
|
||||
return
|
||||
if hass.data.get(DOMAIN):
|
||||
configurator = get_component('configurator')
|
||||
configurator.request_done(hass.data.get(DOMAIN))
|
||||
del hass.data[DOMAIN]
|
||||
player = SpotifyMediaPlayer(oauth, config.get(CONF_NAME, DEFAULT_NAME))
|
||||
add_devices([player], True)
|
||||
|
||||
|
||||
class SpotifyAuthCallbackView(HomeAssistantView):
|
||||
"""Spotify Authorization Callback View."""
|
||||
|
||||
requires_auth = False
|
||||
url = AUTH_CALLBACK_PATH
|
||||
name = AUTH_CALLBACK_NAME
|
||||
|
||||
def __init__(self, config, add_devices, oauth):
|
||||
"""Initialize."""
|
||||
self.config = config
|
||||
self.add_devices = add_devices
|
||||
self.oauth = oauth
|
||||
|
||||
@callback
|
||||
def get(self, request):
|
||||
"""Receive authorization token."""
|
||||
hass = request.app['hass']
|
||||
self.oauth.get_access_token(request.GET['code'])
|
||||
hass.async_add_job(setup_platform, hass, self.config, self.add_devices)
|
||||
|
||||
|
||||
class SpotifyMediaPlayer(MediaPlayerDevice):
|
||||
"""Representation of a Spotify controller."""
|
||||
|
||||
def __init__(self, oauth, name):
|
||||
"""Initialize."""
|
||||
self._name = name
|
||||
self._oauth = oauth
|
||||
self._album = None
|
||||
self._title = None
|
||||
self._artist = None
|
||||
self._uri = None
|
||||
self._image_url = None
|
||||
self._state = STATE_UNKNOWN
|
||||
self._current_device = None
|
||||
self._devices = None
|
||||
self._volume = None
|
||||
self._player = None
|
||||
self._token_info = self._oauth.get_cached_token()
|
||||
|
||||
def refresh_spotify_instance(self):
|
||||
"""Fetch a new spotify instance."""
|
||||
import spotipy
|
||||
token_refreshed = False
|
||||
need_token = (self._token_info is None or
|
||||
self._oauth.is_token_expired(self._token_info))
|
||||
if need_token:
|
||||
new_token = \
|
||||
self._oauth.refresh_access_token(
|
||||
self._token_info['refresh_token'])
|
||||
self._token_info = new_token
|
||||
token_refreshed = True
|
||||
if self._player is None or token_refreshed:
|
||||
self._player = \
|
||||
spotipy.Spotify(auth=self._token_info.get('access_token'))
|
||||
|
||||
def update(self):
|
||||
"""Update state and attributes."""
|
||||
self.refresh_spotify_instance()
|
||||
# Available devices
|
||||
devices = self._player.devices().get('devices')
|
||||
if devices is not None:
|
||||
self._devices = {device.get('name'): device.get('id')
|
||||
for device in devices}
|
||||
# Current playback state
|
||||
current = self._player.current_playback()
|
||||
if current is None:
|
||||
self._state = STATE_IDLE
|
||||
return
|
||||
# Track metadata
|
||||
item = current.get('item')
|
||||
if item:
|
||||
self._album = item.get('album').get('name')
|
||||
self._title = item.get('name')
|
||||
self._artist = ', '.join([artist.get('name')
|
||||
for artist in item.get('artists')])
|
||||
self._uri = current.get('uri')
|
||||
self._image_url = item.get('album').get('images')[0].get('url')
|
||||
# Playing state
|
||||
self._state = STATE_PAUSED
|
||||
if current.get('is_playing'):
|
||||
self._state = STATE_PLAYING
|
||||
device = current.get('device')
|
||||
if device is None:
|
||||
self._state = STATE_IDLE
|
||||
else:
|
||||
if device.get('volume_percent'):
|
||||
self._volume = device.get('volume_percent') / 100
|
||||
if device.get('name'):
|
||||
self._current_device = device.get('name')
|
||||
|
||||
def set_volume_level(self, volume):
|
||||
"""Set the volume level."""
|
||||
self._player.volume(int(volume * 100))
|
||||
|
||||
def media_next_track(self):
|
||||
"""Skip to next track."""
|
||||
self._player.next_track()
|
||||
|
||||
def media_previous_track(self):
|
||||
"""Skip to previous track."""
|
||||
self._player.previous_track()
|
||||
|
||||
def media_play(self):
|
||||
"""Start or resume playback."""
|
||||
self._player.start_playback()
|
||||
|
||||
def media_pause(self):
|
||||
"""Pause playback."""
|
||||
self._player.pause_playback()
|
||||
|
||||
def select_source(self, source):
|
||||
"""Select playback device."""
|
||||
self._player.transfer_playback(self._devices[source])
|
||||
|
||||
def play_media(self, media_type, media_id, **kwargs):
|
||||
"""Play media."""
|
||||
kwargs = {}
|
||||
if media_type == MEDIA_TYPE_MUSIC:
|
||||
kwargs['uris'] = [media_id]
|
||||
elif media_type == MEDIA_TYPE_PLAYLIST:
|
||||
kwargs['context_uri'] = media_id
|
||||
else:
|
||||
_LOGGER.error('media type %s is not supported', media_type)
|
||||
return
|
||||
if not media_id.startswith('spotify:'):
|
||||
_LOGGER.error('media id must be spotify uri')
|
||||
return
|
||||
self._player.start_playback(**kwargs)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Name."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Icon."""
|
||||
return ICON
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Playback state."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def volume_level(self):
|
||||
"""Device volume."""
|
||||
return self._volume
|
||||
|
||||
@property
|
||||
def source_list(self):
|
||||
"""Playback devices."""
|
||||
return list(self._devices.keys())
|
||||
|
||||
@property
|
||||
def source(self):
|
||||
"""Current playback device."""
|
||||
return self._current_device
|
||||
|
||||
@property
|
||||
def media_content_id(self):
|
||||
"""Media URL."""
|
||||
return self._uri
|
||||
|
||||
@property
|
||||
def media_image_url(self):
|
||||
"""Media image url."""
|
||||
return self._image_url
|
||||
|
||||
@property
|
||||
def media_artist(self):
|
||||
"""Media artist."""
|
||||
return self._artist
|
||||
|
||||
@property
|
||||
def media_album_name(self):
|
||||
"""Media album."""
|
||||
return self._album
|
||||
|
||||
@property
|
||||
def media_title(self):
|
||||
"""Media title."""
|
||||
return self._title
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Media player features that are supported."""
|
||||
return SUPPORT_SPOTIFY
|
@ -29,8 +29,6 @@ _LOGGER = logging.getLogger(__name__)
|
||||
DEFAULT_PORT = 9000
|
||||
TIMEOUT = 10
|
||||
|
||||
KNOWN_DEVICES = []
|
||||
|
||||
SUPPORT_SQUEEZEBOX = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | \
|
||||
SUPPORT_VOLUME_MUTE | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | \
|
||||
SUPPORT_SEEK | SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PLAY_MEDIA | \
|
||||
@ -71,18 +69,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
host, port, error)
|
||||
return False
|
||||
|
||||
# Combine it with port to allow multiple servers at the same host
|
||||
key = "{}:{}".format(ipaddr, port)
|
||||
|
||||
# Only add a media server once
|
||||
if key in KNOWN_DEVICES:
|
||||
return False
|
||||
KNOWN_DEVICES.append(key)
|
||||
|
||||
_LOGGER.debug("Creating LMS object for %s", ipaddr)
|
||||
lms = LogitechMediaServer(hass, host, port, username, password)
|
||||
if lms is False:
|
||||
return False
|
||||
|
||||
players = yield from lms.create_players()
|
||||
async_add_devices(players)
|
||||
@ -173,6 +161,11 @@ class SqueezeBoxDevice(MediaPlayerDevice):
|
||||
"""Return the name of the device."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return an unique ID."""
|
||||
return self._id
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
|
@ -5,6 +5,7 @@ For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/media_player.webostv/
|
||||
"""
|
||||
import logging
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
from urllib.parse import urlparse
|
||||
|
||||
@ -24,9 +25,7 @@ from homeassistant.const import (
|
||||
from homeassistant.loader import get_component
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['https://github.com/TheRealLink/pylgtv'
|
||||
'/archive/v0.1.4.zip'
|
||||
'#pylgtv==0.1.4',
|
||||
REQUIREMENTS = ['pylgtv==0.1.6',
|
||||
'websockets==3.2',
|
||||
'wakeonlan==0.2.2']
|
||||
|
||||
@ -101,7 +100,8 @@ def setup_tv(host, mac, name, customize, config, hass, add_devices):
|
||||
_LOGGER.warning(
|
||||
"Connected to LG webOS TV %s but not paired", host)
|
||||
return
|
||||
except (OSError, ConnectionClosed):
|
||||
except (OSError, ConnectionClosed, TypeError,
|
||||
asyncio.TimeoutError):
|
||||
_LOGGER.error("Unable to connect to host %s", host)
|
||||
return
|
||||
else:
|
||||
@ -198,7 +198,8 @@ class LgWebOSDevice(MediaPlayerDevice):
|
||||
app = self._app_list[source['appId']]
|
||||
self._source_list[app['title']] = app
|
||||
|
||||
except (OSError, ConnectionClosed):
|
||||
except (OSError, ConnectionClosed, TypeError,
|
||||
asyncio.TimeoutError):
|
||||
self._state = STATE_OFF
|
||||
|
||||
@property
|
||||
@ -240,7 +241,10 @@ class LgWebOSDevice(MediaPlayerDevice):
|
||||
def media_image_url(self):
|
||||
"""Image url of current playing media."""
|
||||
if self._current_source_id in self._app_list:
|
||||
return self._app_list[self._current_source_id]['largeIcon']
|
||||
icon = self._app_list[self._current_source_id]['largeIcon']
|
||||
if not icon.startswith('http'):
|
||||
icon = self._app_list[self._current_source_id]['icon']
|
||||
return icon
|
||||
return None
|
||||
|
||||
@property
|
||||
@ -256,7 +260,8 @@ class LgWebOSDevice(MediaPlayerDevice):
|
||||
self._state = STATE_OFF
|
||||
try:
|
||||
self._client.power_off()
|
||||
except (OSError, ConnectionClosed):
|
||||
except (OSError, ConnectionClosed, TypeError,
|
||||
asyncio.TimeoutError):
|
||||
pass
|
||||
|
||||
def turn_on(self):
|
||||
|
@ -59,10 +59,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
zone_ignore = config.get(CONF_ZONE_IGNORE)
|
||||
|
||||
if discovery_info is not None:
|
||||
name = discovery_info[0]
|
||||
model = discovery_info[1]
|
||||
ctrl_url = discovery_info[2]
|
||||
desc_url = discovery_info[3]
|
||||
name = discovery_info.get('name')
|
||||
model = discovery_info.get('model_name')
|
||||
ctrl_url = discovery_info.get('control_url')
|
||||
desc_url = discovery_info.get('description_url')
|
||||
if ctrl_url in hass.data[KNOWN]:
|
||||
_LOGGER.info("%s already manually configured", ctrl_url)
|
||||
return
|
||||
|
@ -158,6 +158,15 @@ class ModbusHub(object):
|
||||
count,
|
||||
**kwargs)
|
||||
|
||||
def read_input_registers(self, unit, address, count):
|
||||
"""Read input registers."""
|
||||
with self._lock:
|
||||
kwargs = {'unit': unit} if unit else {}
|
||||
return self._client.read_input_registers(
|
||||
address,
|
||||
count,
|
||||
**kwargs)
|
||||
|
||||
def read_holding_registers(self, unit, address, count):
|
||||
"""Read holding registers."""
|
||||
with self._lock:
|
||||
|
@ -28,7 +28,7 @@ from homeassistant.const import (
|
||||
CONF_PASSWORD, CONF_PORT, CONF_PROTOCOL, CONF_PAYLOAD)
|
||||
from homeassistant.components.mqtt.server import HBMQTT_CONFIG_SCHEMA
|
||||
|
||||
REQUIREMENTS = ['paho-mqtt==1.2.1']
|
||||
REQUIREMENTS = ['paho-mqtt==1.2.2']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -201,7 +201,8 @@ def publish_template(hass, topic, payload_template, qos=None, retain=None):
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_subscribe(hass, topic, msg_callback, qos=DEFAULT_QOS):
|
||||
def async_subscribe(hass, topic, msg_callback, qos=DEFAULT_QOS,
|
||||
encoding='utf-8'):
|
||||
"""Subscribe to an MQTT topic."""
|
||||
@callback
|
||||
def async_mqtt_topic_subscriber(dp_topic, dp_payload, dp_qos):
|
||||
@ -209,7 +210,21 @@ def async_subscribe(hass, topic, msg_callback, qos=DEFAULT_QOS):
|
||||
if not _match_topic(topic, dp_topic):
|
||||
return
|
||||
|
||||
hass.async_run_job(msg_callback, dp_topic, dp_payload, dp_qos)
|
||||
if encoding is not None:
|
||||
try:
|
||||
payload = dp_payload.decode(encoding)
|
||||
_LOGGER.debug("Received message on %s: %s",
|
||||
dp_topic, payload)
|
||||
except (AttributeError, UnicodeDecodeError):
|
||||
_LOGGER.error("Illegal payload encoding %s from "
|
||||
"MQTT topic: %s, Payload: %s",
|
||||
encoding, dp_topic, dp_payload)
|
||||
return
|
||||
else:
|
||||
_LOGGER.debug("Received binary message on %s", dp_topic)
|
||||
payload = dp_payload
|
||||
|
||||
hass.async_run_job(msg_callback, dp_topic, payload, dp_qos)
|
||||
|
||||
async_remove = async_dispatcher_connect(
|
||||
hass, SIGNAL_MQTT_MESSAGE_RECEIVED, async_mqtt_topic_subscriber)
|
||||
@ -218,10 +233,12 @@ def async_subscribe(hass, topic, msg_callback, qos=DEFAULT_QOS):
|
||||
return async_remove
|
||||
|
||||
|
||||
def subscribe(hass, topic, msg_callback, qos=DEFAULT_QOS):
|
||||
def subscribe(hass, topic, msg_callback, qos=DEFAULT_QOS,
|
||||
encoding='utf-8'):
|
||||
"""Subscribe to an MQTT topic."""
|
||||
async_remove = run_coroutine_threadsafe(
|
||||
async_subscribe(hass, topic, msg_callback, qos),
|
||||
async_subscribe(hass, topic, msg_callback,
|
||||
qos, encoding),
|
||||
hass.loop
|
||||
).result()
|
||||
|
||||
@ -372,16 +389,16 @@ def async_setup(hass, config):
|
||||
payload_template = call.data.get(ATTR_PAYLOAD_TEMPLATE)
|
||||
qos = call.data[ATTR_QOS]
|
||||
retain = call.data[ATTR_RETAIN]
|
||||
try:
|
||||
if payload_template is not None:
|
||||
if payload_template is not None:
|
||||
try:
|
||||
payload = \
|
||||
template.Template(payload_template, hass).async_render()
|
||||
except template.jinja2.TemplateError as exc:
|
||||
_LOGGER.error(
|
||||
"Unable to publish to '%s': rendering payload template of "
|
||||
"'%s' failed because %s",
|
||||
msg_topic, payload_template, exc)
|
||||
return
|
||||
except template.jinja2.TemplateError as exc:
|
||||
_LOGGER.error(
|
||||
"Unable to publish to '%s': rendering payload template of "
|
||||
"'%s' failed because %s",
|
||||
msg_topic, payload_template, exc)
|
||||
return
|
||||
|
||||
yield from hass.data[DATA_MQTT].async_publish(
|
||||
msg_topic, payload, qos, retain)
|
||||
@ -564,18 +581,10 @@ class MQTT(object):
|
||||
|
||||
def _mqtt_on_message(self, _mqttc, _userdata, msg):
|
||||
"""Message received callback."""
|
||||
try:
|
||||
payload = msg.payload.decode('utf-8')
|
||||
except (AttributeError, UnicodeDecodeError):
|
||||
_LOGGER.error("Illegal utf-8 unicode payload from "
|
||||
"MQTT topic: %s, Payload: %s", msg.topic,
|
||||
msg.payload)
|
||||
else:
|
||||
_LOGGER.info("Received message on %s: %s", msg.topic, payload)
|
||||
dispatcher_send(
|
||||
self.hass, SIGNAL_MQTT_MESSAGE_RECEIVED, msg.topic, payload,
|
||||
msg.qos
|
||||
)
|
||||
dispatcher_send(
|
||||
self.hass, SIGNAL_MQTT_MESSAGE_RECEIVED, msg.topic, msg.payload,
|
||||
msg.qos
|
||||
)
|
||||
|
||||
def _mqtt_on_unsubscribe(self, _mqttc, _userdata, mid, granted_qos):
|
||||
"""Unsubscribe successful callback."""
|
||||
|
@ -17,12 +17,13 @@ import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
REQUIREMENTS = ['https://github.com/jabesq/pybotvac/archive/v0.0.1.zip'
|
||||
'#pybotvac==0.0.1']
|
||||
REQUIREMENTS = ['https://github.com/jabesq/pybotvac/archive/v0.0.3.zip'
|
||||
'#pybotvac==0.0.3']
|
||||
|
||||
DOMAIN = 'neato'
|
||||
NEATO_ROBOTS = 'neato_robots'
|
||||
NEATO_LOGIN = 'neato_login'
|
||||
NEATO_MAP_DATA = 'neato_map_data'
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
@ -80,7 +81,7 @@ ALERTS = {
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Setup the Verisure component."""
|
||||
"""Setup the Neato component."""
|
||||
from pybotvac import Account
|
||||
|
||||
hass.data[NEATO_LOGIN] = NeatoHub(hass, config[DOMAIN], Account)
|
||||
@ -89,7 +90,7 @@ def setup(hass, config):
|
||||
_LOGGER.debug('Failed to login to Neato API')
|
||||
return False
|
||||
hub.update_robots()
|
||||
for component in ('sensor', 'switch'):
|
||||
for component in ('camera', 'sensor', 'switch'):
|
||||
discovery.load_platform(hass, component, DOMAIN, {}, config)
|
||||
|
||||
return True
|
||||
@ -108,6 +109,7 @@ class NeatoHub(object):
|
||||
domain_config[CONF_USERNAME],
|
||||
domain_config[CONF_PASSWORD])
|
||||
self._hass.data[NEATO_ROBOTS] = self.my_neato.robots
|
||||
self._hass.data[NEATO_MAP_DATA] = self.my_neato.maps
|
||||
|
||||
def login(self):
|
||||
"""Login to My Neato."""
|
||||
@ -126,3 +128,9 @@ class NeatoHub(object):
|
||||
_LOGGER.debug('Running HUB.update_robots %s',
|
||||
self._hass.data[NEATO_ROBOTS])
|
||||
self._hass.data[NEATO_ROBOTS] = self.my_neato.robots
|
||||
self._hass.data[NEATO_MAP_DATA] = self.my_neato.maps
|
||||
|
||||
def download_map(self, url):
|
||||
"""Download a new map image."""
|
||||
map_image_data = self.my_neato.get_map_image(url)
|
||||
return map_image_data
|
||||
|
@ -13,7 +13,7 @@ from homeassistant.components.notify import (
|
||||
from homeassistant.const import (CONF_API_KEY, CONF_SENDER, CONF_RECIPIENT)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['sendgrid==3.6.5']
|
||||
REQUIREMENTS = ['sendgrid==4.0.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -14,8 +14,7 @@ from homeassistant.components.notify import (
|
||||
ATTR_DATA, BaseNotificationService, PLATFORM_SCHEMA)
|
||||
from homeassistant.const import (CONF_FILENAME, CONF_HOST, CONF_ICON)
|
||||
|
||||
REQUIREMENTS = ['https://github.com/TheRealLink/pylgtv/archive/v0.1.4.zip'
|
||||
'#pylgtv==0.1.4']
|
||||
REQUIREMENTS = ['pylgtv==0.1.6']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -8,6 +8,7 @@ import asyncio
|
||||
import logging
|
||||
|
||||
import aiohttp
|
||||
from aiohttp import hdrs
|
||||
import async_timeout
|
||||
import voluptuous as vol
|
||||
|
||||
@ -31,6 +32,8 @@ SUPPORT_REST_METHODS = [
|
||||
'delete',
|
||||
]
|
||||
|
||||
CONF_CONTENT_TYPE = 'content_type'
|
||||
|
||||
COMMAND_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_URL): cv.template,
|
||||
vol.Optional(CONF_METHOD, default=DEFAULT_METHOD):
|
||||
@ -39,6 +42,7 @@ COMMAND_SCHEMA = vol.Schema({
|
||||
vol.Inclusive(CONF_PASSWORD, 'authentication'): cv.string,
|
||||
vol.Optional(CONF_PAYLOAD): cv.template,
|
||||
vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): vol.Coerce(int),
|
||||
vol.Optional(CONF_CONTENT_TYPE): cv.string
|
||||
})
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
@ -72,6 +76,11 @@ def async_setup(hass, config):
|
||||
template_payload = command_config[CONF_PAYLOAD]
|
||||
template_payload.hass = hass
|
||||
|
||||
headers = None
|
||||
if CONF_CONTENT_TYPE in command_config:
|
||||
content_type = command_config[CONF_CONTENT_TYPE]
|
||||
headers = {hdrs.CONTENT_TYPE: content_type}
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_service_handler(service):
|
||||
"""Execute a shell command service."""
|
||||
@ -86,7 +95,8 @@ def async_setup(hass, config):
|
||||
request = yield from getattr(websession, method)(
|
||||
template_url.async_render(variables=service.data),
|
||||
data=payload,
|
||||
auth=auth
|
||||
auth=auth,
|
||||
headers=headers
|
||||
)
|
||||
|
||||
if request.status < 400:
|
||||
|
97
homeassistant/components/scene/lifx_cloud.py
Normal file
97
homeassistant/components/scene/lifx_cloud.py
Normal file
@ -0,0 +1,97 @@
|
||||
"""
|
||||
Support for LIFX Cloud scenes.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/scene.lifx_cloud/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import aiohttp
|
||||
import async_timeout
|
||||
|
||||
from homeassistant.components.scene import Scene
|
||||
from homeassistant.const import (CONF_PLATFORM, CONF_TOKEN, CONF_TIMEOUT)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import (async_get_clientsession)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
LIFX_API_URL = 'https://api.lifx.com/v1/{0}'
|
||||
DEFAULT_TIMEOUT = 10
|
||||
|
||||
PLATFORM_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_PLATFORM): 'lifx_cloud',
|
||||
vol.Required(CONF_TOKEN): cv.string,
|
||||
vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
|
||||
})
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Setup the scenes stored in the LIFX Cloud."""
|
||||
token = config.get(CONF_TOKEN)
|
||||
timeout = config.get(CONF_TIMEOUT)
|
||||
|
||||
headers = {
|
||||
"Authorization": "Bearer %s" % token,
|
||||
}
|
||||
|
||||
url = LIFX_API_URL.format('scenes')
|
||||
|
||||
try:
|
||||
httpsession = async_get_clientsession(hass)
|
||||
with async_timeout.timeout(timeout, loop=hass.loop):
|
||||
scenes_resp = yield from httpsession.get(url, headers=headers)
|
||||
|
||||
except (asyncio.TimeoutError, aiohttp.ClientError):
|
||||
_LOGGER.exception("Error on %s", url)
|
||||
return False
|
||||
|
||||
status = scenes_resp.status
|
||||
if status == 200:
|
||||
data = yield from scenes_resp.json()
|
||||
devices = []
|
||||
for scene in data:
|
||||
devices.append(LifxCloudScene(hass, headers, timeout, scene))
|
||||
async_add_devices(devices)
|
||||
return True
|
||||
elif status == 401:
|
||||
_LOGGER.error("Unauthorized (bad token?) on %s", url)
|
||||
return False
|
||||
else:
|
||||
_LOGGER.error("HTTP error %d on %s", scenes_resp.status, url)
|
||||
return False
|
||||
|
||||
|
||||
class LifxCloudScene(Scene):
|
||||
"""Representation of a LIFX Cloud scene."""
|
||||
|
||||
def __init__(self, hass, headers, timeout, scene_data):
|
||||
"""Initialize the scene."""
|
||||
self.hass = hass
|
||||
self._headers = headers
|
||||
self._timeout = timeout
|
||||
self._name = scene_data["name"]
|
||||
self._uuid = scene_data["uuid"]
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the scene."""
|
||||
return self._name
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_activate(self):
|
||||
"""Activate the scene."""
|
||||
url = LIFX_API_URL.format('scenes/scene_id:%s/activate' % self._uuid)
|
||||
|
||||
try:
|
||||
httpsession = async_get_clientsession(self.hass)
|
||||
with async_timeout.timeout(self._timeout, loop=self.hass.loop):
|
||||
yield from httpsession.put(url, headers=self._headers)
|
||||
|
||||
except (asyncio.TimeoutError, aiohttp.ClientError):
|
||||
_LOGGER.exception("Error on %s", url)
|
75
homeassistant/components/sensor/alarmdecoder.py
Normal file
75
homeassistant/components/sensor/alarmdecoder.py
Normal file
@ -0,0 +1,75 @@
|
||||
"""
|
||||
Support for AlarmDecoder Sensors (Shows Panel Display).
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/sensor.alarmdecoder/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
from homeassistant.components.alarmdecoder import (SIGNAL_PANEL_MESSAGE)
|
||||
|
||||
from homeassistant.const import (STATE_UNKNOWN)
|
||||
|
||||
DEPENDENCIES = ['alarmdecoder']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Perform the setup for AlarmDecoder sensor devices."""
|
||||
_LOGGER.debug("AlarmDecoderSensor: async_setup_platform")
|
||||
|
||||
device = AlarmDecoderSensor(hass)
|
||||
|
||||
async_add_devices([device])
|
||||
|
||||
|
||||
class AlarmDecoderSensor(Entity):
|
||||
"""Representation of an AlarmDecoder keypad."""
|
||||
|
||||
def __init__(self, hass):
|
||||
"""Initialize the alarm panel."""
|
||||
self._display = ""
|
||||
self._state = STATE_UNKNOWN
|
||||
self._icon = 'mdi:alarm-check'
|
||||
self._name = 'Alarm Panel Display'
|
||||
|
||||
_LOGGER.debug("AlarmDecoderSensor: Setting up panel")
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
async_dispatcher_connect(
|
||||
self.hass, SIGNAL_PANEL_MESSAGE, self._message_callback)
|
||||
|
||||
@callback
|
||||
def _message_callback(self, message):
|
||||
if self._display != message.text:
|
||||
self._display = message.text
|
||||
self.hass.async_add_job(self.async_update_ha_state())
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Return the icon if any."""
|
||||
return self._icon
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the overall state."""
|
||||
return self._display
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed."""
|
||||
return False
|
@ -19,7 +19,7 @@ import homeassistant.loader as loader
|
||||
|
||||
from requests.exceptions import HTTPError, ConnectTimeout
|
||||
|
||||
REQUIREMENTS = ['amcrest==1.1.8']
|
||||
REQUIREMENTS = ['amcrest==1.1.9']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -116,6 +116,7 @@ class ArwnSensor(Entity):
|
||||
"""Update the sensor with the most recent event."""
|
||||
self.event = {}
|
||||
self.event.update(event)
|
||||
self.hass.async_add_job(self.async_update_ha_state())
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
|
@ -13,13 +13,13 @@ from homeassistant.const import CONF_NAME
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
REQUIREMENTS = ['py-cpuinfo==3.0.0']
|
||||
REQUIREMENTS = ['py-cpuinfo==3.2.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_BRAND = 'Brand'
|
||||
ATTR_HZ = 'GHz Advertised'
|
||||
ATTR_VENDOR = 'Vendor ID'
|
||||
ATTR_ARCH = 'arch'
|
||||
|
||||
DEFAULT_NAME = 'CPU speed'
|
||||
ICON = 'mdi:pulse'
|
||||
@ -67,7 +67,7 @@ class CpuSpeedSensor(Entity):
|
||||
"""Return the state attributes."""
|
||||
if self.info is not None:
|
||||
return {
|
||||
ATTR_VENDOR: self.info['vendor_id'],
|
||||
ATTR_ARCH: self.info['arch'],
|
||||
ATTR_BRAND: self.info['brand'],
|
||||
ATTR_HZ: round(self.info['hz_advertised_raw'][0]/10**9, 2)
|
||||
}
|
||||
|
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