Merge pull request #7197 from home-assistant/release-0-43

0.43
This commit is contained in:
Paulus Schoutsen 2017-04-22 00:33:01 -07:00 committed by GitHub
commit 8be2ac70ec
181 changed files with 6406 additions and 2083 deletions

View File

@ -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

View File

@ -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 . .

View File

@ -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],

View 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))

View 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

View File

@ -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."""

View File

@ -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."""

View File

@ -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

View 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())

View File

@ -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

View 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)

View File

@ -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)

View File

@ -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__)

View 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)

View 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

View File

@ -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

View File

@ -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."""

View File

@ -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):

View File

@ -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))

View File

@ -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)

View File

@ -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:

View 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

View File

@ -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):

View File

@ -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

View File

@ -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"
}

File diff suppressed because one or more lines are too long

@ -1 +1 @@
Subproject commit abbdc6f055524c5d3ed0bb50e35400fed40d573f
Subproject commit 3fdba359865823805e8ea756c8500d3913976158

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -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'])

View File

@ -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

View File

@ -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:

View File

@ -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)

View File

@ -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."""

View 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

View 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'

View File

@ -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):

View File

@ -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

View 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

View 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))

View 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)

View File

@ -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:

View File

@ -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 = ''

View File

@ -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):

View File

@ -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

View File

@ -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):

View File

@ -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'

View File

@ -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)]

View File

@ -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)

View File

@ -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:

View File

@ -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

View File

@ -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."""

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -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

View File

@ -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'

View File

@ -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):

View File

@ -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])

View 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

View File

@ -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."""

View File

@ -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):

View File

@ -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

View File

@ -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:

View File

@ -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."""

View File

@ -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

View File

@ -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__)

View File

@ -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__)

View File

@ -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:

View 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)

View 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

View File

@ -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__)

View File

@ -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):

View File

@ -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