Merge branch 'dev' into master

This commit is contained in:
Paulus Schoutsen 2016-12-11 22:49:06 -08:00 committed by GitHub
commit ab92a91ac5
86 changed files with 2911 additions and 632 deletions

View File

@ -122,6 +122,7 @@ omit =
homeassistant/components/alarm_control_panel/simplisafe.py homeassistant/components/alarm_control_panel/simplisafe.py
homeassistant/components/binary_sensor/arest.py homeassistant/components/binary_sensor/arest.py
homeassistant/components/binary_sensor/concord232.py homeassistant/components/binary_sensor/concord232.py
homeassistant/components/binary_sensor/flic.py
homeassistant/components/binary_sensor/rest.py homeassistant/components/binary_sensor/rest.py
homeassistant/components/browser.py homeassistant/components/browser.py
homeassistant/components/camera/amcrest.py homeassistant/components/camera/amcrest.py
@ -183,6 +184,7 @@ omit =
homeassistant/components/light/x10.py homeassistant/components/light/x10.py
homeassistant/components/light/yeelight.py homeassistant/components/light/yeelight.py
homeassistant/components/lirc.py homeassistant/components/lirc.py
homeassistant/components/media_player/aquostv.py
homeassistant/components/media_player/braviatv.py homeassistant/components/media_player/braviatv.py
homeassistant/components/media_player/cast.py homeassistant/components/media_player/cast.py
homeassistant/components/media_player/cmus.py homeassistant/components/media_player/cmus.py
@ -210,6 +212,7 @@ omit =
homeassistant/components/media_player/snapcast.py homeassistant/components/media_player/snapcast.py
homeassistant/components/media_player/sonos.py homeassistant/components/media_player/sonos.py
homeassistant/components/media_player/squeezebox.py homeassistant/components/media_player/squeezebox.py
homeassistant/components/media_player/vlc.py
homeassistant/components/media_player/yamaha.py homeassistant/components/media_player/yamaha.py
homeassistant/components/notify/aws_lambda.py homeassistant/components/notify/aws_lambda.py
homeassistant/components/notify/aws_sns.py homeassistant/components/notify/aws_sns.py
@ -280,6 +283,7 @@ omit =
homeassistant/components/sensor/mhz19.py homeassistant/components/sensor/mhz19.py
homeassistant/components/sensor/miflora.py homeassistant/components/sensor/miflora.py
homeassistant/components/sensor/mqtt_room.py homeassistant/components/sensor/mqtt_room.py
homeassistant/components/sensor/netdata.py
homeassistant/components/sensor/neurio_energy.py homeassistant/components/sensor/neurio_energy.py
homeassistant/components/sensor/nut.py homeassistant/components/sensor/nut.py
homeassistant/components/sensor/nzbget.py homeassistant/components/sensor/nzbget.py
@ -292,6 +296,7 @@ omit =
homeassistant/components/sensor/pvoutput.py homeassistant/components/sensor/pvoutput.py
homeassistant/components/sensor/sabnzbd.py homeassistant/components/sensor/sabnzbd.py
homeassistant/components/sensor/scrape.py homeassistant/components/sensor/scrape.py
homeassistant/components/sensor/sensehat.py
homeassistant/components/sensor/serial_pm.py homeassistant/components/sensor/serial_pm.py
homeassistant/components/sensor/snmp.py homeassistant/components/sensor/snmp.py
homeassistant/components/sensor/sonarr.py homeassistant/components/sensor/sonarr.py
@ -313,10 +318,11 @@ omit =
homeassistant/components/sensor/waqi.py homeassistant/components/sensor/waqi.py
homeassistant/components/sensor/xbox_live.py homeassistant/components/sensor/xbox_live.py
homeassistant/components/sensor/yweather.py homeassistant/components/sensor/yweather.py
homeassistant/components/sensor/waqi.py homeassistant/components/sensor/zamg.py
homeassistant/components/switch/acer_projector.py homeassistant/components/switch/acer_projector.py
homeassistant/components/switch/anel_pwrctrl.py homeassistant/components/switch/anel_pwrctrl.py
homeassistant/components/switch/arest.py homeassistant/components/switch/arest.py
homeassistant/components/switch/digitalloggers.py
homeassistant/components/switch/dlink.py homeassistant/components/switch/dlink.py
homeassistant/components/switch/edimax.py homeassistant/components/switch/edimax.py
homeassistant/components/switch/hikvisioncam.py homeassistant/components/switch/hikvisioncam.py

View File

@ -1,13 +1,14 @@
# Contributing to Home Assistant # Contributing to Home Assistant
Everybody is invited and welcome to contribute to Home Assistant. There is a lot to do...if you are not a developer perhaps you would like to help with the documentation on [home-assistant.io](https://home-assistant.io/)? If you are a developer and have devices in your home which aren't working with Home Assistant yet, why not spent a couple of hours and help to integrate them? Everybody is invited and welcome to contribute to Home Assistant. There is a lot to do...if you are not a developer perhaps you would like to help with the documentation on [home-assistant.io](https://home-assistant.io/)? If you are a developer and have devices in your home which aren't working with Home Assistant yet, why not spent a couple of hours and help to integrate them?
The process is straight-forward. The process is straight-forward.
- Read [How to get faster PR reviews](https://github.com/kubernetes/kubernetes/blob/master/docs/devel/faster_reviews.md) by Kubernetes (but skip step 0)
- Fork the Home Assistant [git repository](https://github.com/home-assistant/home-assistant). - Fork the Home Assistant [git repository](https://github.com/home-assistant/home-assistant).
- Write the code for your device, notification service, sensor, or IoT thing. - Write the code for your device, notification service, sensor, or IoT thing.
- Ensure tests work. - Ensure tests work.
- Create a Pull Request against the [**dev**](https://github.com/home-assistant/home-assistant/tree/dev) branch of Home Assistant. - Create a Pull Request against the [**dev**](https://github.com/home-assistant/home-assistant/tree/dev) branch of Home Assistant.
Still interested? Then you should take a peak at the [developer documentation](https://home-assistant.io/developers/) to get more details. Still interested? Then you should take a peak at the [developer documentation](https://home-assistant.io/developers/) to get more details.

View File

@ -8,9 +8,12 @@ WORKDIR /usr/src/app
RUN pip3 install --no-cache-dir colorlog cython RUN pip3 install --no-cache-dir colorlog cython
# For the nmap tracker, bluetooth tracker, Z-Wave # For the nmap tracker, bluetooth tracker, Z-Wave, tellstick
RUN apt-get update && \ RUN echo "deb http://download.telldus.com/debian/ stable main" >> /etc/apt/sources.list.d/telldus.list && \
apt-get install -y --no-install-recommends nmap net-tools cython3 libudev-dev sudo libglib2.0-dev bluetooth libbluetooth-dev && \ wget -qO - http://download.telldus.se/debian/telldus-public.key | apt-key add - && \
apt-get update && \
apt-get install -y --no-install-recommends nmap net-tools cython3 libudev-dev sudo libglib2.0-dev bluetooth libbluetooth-dev \
libtelldus-core2 libtelldus-core-dev && \
apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
COPY script/build_python_openzwave script/build_python_openzwave COPY script/build_python_openzwave script/build_python_openzwave

View File

@ -4,6 +4,7 @@ Component to interface with an alarm control panel.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/alarm_control_panel/ https://home-assistant.io/components/alarm_control_panel/
""" """
import asyncio
import logging import logging
import os import os
@ -42,40 +43,6 @@ ALARM_SERVICE_SCHEMA = vol.Schema({
}) })
def setup(hass, config):
"""Track states and offer events for sensors."""
component = EntityComponent(
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL)
component.setup(config)
def alarm_service_handler(service):
"""Map services to methods on Alarm."""
target_alarms = component.extract_from_service(service)
code = service.data.get(ATTR_CODE)
method = SERVICE_TO_METHOD[service.service]
for alarm in target_alarms:
getattr(alarm, method)(code)
for alarm in target_alarms:
if not alarm.should_poll:
continue
alarm.update_ha_state(True)
descriptions = load_yaml_config_file(
os.path.join(os.path.dirname(__file__), 'services.yaml'))
for service in SERVICE_TO_METHOD:
hass.services.register(DOMAIN, service, alarm_service_handler,
descriptions.get(service),
schema=ALARM_SERVICE_SCHEMA)
return True
def alarm_disarm(hass, code=None, entity_id=None): def alarm_disarm(hass, code=None, entity_id=None):
"""Send the alarm the command for disarm.""" """Send the alarm the command for disarm."""
data = {} data = {}
@ -120,6 +87,53 @@ def alarm_trigger(hass, code=None, entity_id=None):
hass.services.call(DOMAIN, SERVICE_ALARM_TRIGGER, data) hass.services.call(DOMAIN, SERVICE_ALARM_TRIGGER, data)
@asyncio.coroutine
def async_setup(hass, config):
"""Track states and offer events for sensors."""
component = EntityComponent(
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL)
yield from component.async_setup(config)
@asyncio.coroutine
def async_alarm_service_handler(service):
"""Map services to methods on Alarm."""
target_alarms = component.async_extract_from_service(service)
code = service.data.get(ATTR_CODE)
method = "async_{}".format(SERVICE_TO_METHOD[service.service])
for alarm in target_alarms:
yield from getattr(alarm, method)(code)
update_tasks = []
for alarm in target_alarms:
if not alarm.should_poll:
continue
update_coro = hass.loop.create_task(
alarm.async_update_ha_state(True))
if hasattr(alarm, 'async_update'):
update_tasks.append(hass.loop.create_task(update_coro))
else:
yield from update_coro
if update_tasks:
yield from asyncio.wait(update_tasks, loop=hass.loop)
descriptions = yield from hass.loop.run_in_executor(
None, load_yaml_config_file, os.path.join(
os.path.dirname(__file__), 'services.yaml'))
for service in SERVICE_TO_METHOD:
hass.services.async_register(
DOMAIN, service, async_alarm_service_handler,
descriptions.get(service), schema=ALARM_SERVICE_SCHEMA)
return True
# pylint: disable=no-self-use # pylint: disable=no-self-use
class AlarmControlPanel(Entity): class AlarmControlPanel(Entity):
"""An abstract class for alarm control devices.""" """An abstract class for alarm control devices."""
@ -138,18 +152,42 @@ class AlarmControlPanel(Entity):
"""Send disarm command.""" """Send disarm command."""
raise NotImplementedError() raise NotImplementedError()
@asyncio.coroutine
def async_alarm_disarm(self, code=None):
"""Send disarm command."""
yield from self.hass.loop.run_in_executor(
None, self.alarm_disarm, code)
def alarm_arm_home(self, code=None): def alarm_arm_home(self, code=None):
"""Send arm home command.""" """Send arm home command."""
raise NotImplementedError() raise NotImplementedError()
@asyncio.coroutine
def async_alarm_arm_home(self, code=None):
"""Send arm home command."""
yield from self.hass.loop.run_in_executor(
None, self.alarm_arm_home, code)
def alarm_arm_away(self, code=None): def alarm_arm_away(self, code=None):
"""Send arm away command.""" """Send arm away command."""
raise NotImplementedError() raise NotImplementedError()
@asyncio.coroutine
def async_alarm_arm_away(self, code=None):
"""Send arm away command."""
yield from self.hass.loop.run_in_executor(
None, self.alarm_arm_away, code)
def alarm_trigger(self, code=None): def alarm_trigger(self, code=None):
"""Send alarm trigger command.""" """Send alarm trigger command."""
raise NotImplementedError() raise NotImplementedError()
@asyncio.coroutine
def async_alarm_trigger(self, code=None):
"""Send alarm trigger command."""
yield from self.hass.loop.run_in_executor(
None, self.alarm_trigger, code)
@property @property
def state_attributes(self): def state_attributes(self):
"""Return the state attributes.""" """Return the state attributes."""

View File

@ -56,11 +56,6 @@ class AlarmDotCom(alarm.AlarmControlPanel):
self._password = password self._password = password
self._state = STATE_UNKNOWN self._state = STATE_UNKNOWN
@property
def should_poll(self):
"""No polling needed."""
return True
def update(self): def update(self):
"""Fetch the latest state.""" """Fetch the latest state."""
self._state = self._alarm.state self._state = self._alarm.state

View File

@ -71,11 +71,6 @@ class Concord232Alarm(alarm.AlarmControlPanel):
self._alarm.last_partition_update = datetime.datetime.now() self._alarm.last_partition_update = datetime.datetime.now()
self.update() self.update()
@property
def should_poll(self):
"""Polling needed."""
return True
@property @property
def name(self): def name(self):
"""Return the name of the device.""" """Return the name of the device."""
@ -126,7 +121,3 @@ class Concord232Alarm(alarm.AlarmControlPanel):
def alarm_arm_away(self, code=None): def alarm_arm_away(self, code=None):
"""Send arm away command.""" """Send arm away command."""
self._alarm.arm('auto') self._alarm.arm('auto')
def alarm_trigger(self, code=None):
"""Alarm trigger command."""
raise NotImplementedError()

View File

@ -97,7 +97,7 @@ class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel):
def _update_callback(self, partition): def _update_callback(self, partition):
"""Update HA state, if needed.""" """Update HA state, if needed."""
if partition is None or int(partition) == self._partition_number: if partition is None or int(partition) == self._partition_number:
self.hass.async_add_job(self.update_ha_state) self.hass.async_add_job(self.async_update_ha_state())
@property @property
def code_format(self): def code_format(self):

View File

@ -116,7 +116,7 @@ class ManualAlarm(alarm.AlarmControlPanel):
self._state = STATE_ALARM_DISARMED self._state = STATE_ALARM_DISARMED
self._state_ts = dt_util.utcnow() self._state_ts = dt_util.utcnow()
self.update_ha_state() self.schedule_update_ha_state()
def alarm_arm_home(self, code=None): def alarm_arm_home(self, code=None):
"""Send arm home command.""" """Send arm home command."""
@ -125,7 +125,7 @@ class ManualAlarm(alarm.AlarmControlPanel):
self._state = STATE_ALARM_ARMED_HOME self._state = STATE_ALARM_ARMED_HOME
self._state_ts = dt_util.utcnow() self._state_ts = dt_util.utcnow()
self.update_ha_state() self.schedule_update_ha_state()
if self._pending_time: if self._pending_time:
track_point_in_time( track_point_in_time(
@ -139,7 +139,7 @@ class ManualAlarm(alarm.AlarmControlPanel):
self._state = STATE_ALARM_ARMED_AWAY self._state = STATE_ALARM_ARMED_AWAY
self._state_ts = dt_util.utcnow() self._state_ts = dt_util.utcnow()
self.update_ha_state() self.schedule_update_ha_state()
if self._pending_time: if self._pending_time:
track_point_in_time( track_point_in_time(
@ -151,7 +151,7 @@ class ManualAlarm(alarm.AlarmControlPanel):
self._pre_trigger_state = self._state self._pre_trigger_state = self._state
self._state = STATE_ALARM_TRIGGERED self._state = STATE_ALARM_TRIGGERED
self._state_ts = dt_util.utcnow() self._state_ts = dt_util.utcnow()
self.update_ha_state() self.schedule_update_ha_state()
if self._trigger_time: if self._trigger_time:
track_point_in_time( track_point_in_time(

View File

@ -62,11 +62,6 @@ class NX584Alarm(alarm.AlarmControlPanel):
self._alarm.list_zones() self._alarm.list_zones()
self._state = STATE_UNKNOWN self._state = STATE_UNKNOWN
@property
def should_poll(self):
"""Polling needed."""
return True
@property @property
def name(self): def name(self):
"""Return the name of the device.""" """Return the name of the device."""
@ -122,7 +117,3 @@ class NX584Alarm(alarm.AlarmControlPanel):
def alarm_arm_away(self, code=None): def alarm_arm_away(self, code=None):
"""Send arm away command.""" """Send arm away command."""
self._alarm.arm('exit') self._alarm.arm('exit')
def alarm_trigger(self, code=None):
"""Alarm trigger command."""
raise NotImplementedError()

View File

@ -61,11 +61,6 @@ class SimpliSafeAlarm(alarm.AlarmControlPanel):
else: else:
self._state = STATE_UNKNOWN self._state = STATE_UNKNOWN
@property
def should_poll(self):
"""Poll the SimpliSafe API."""
return True
@property @property
def name(self): def name(self):
"""Return the name of the device.""" """Return the name of the device."""
@ -104,7 +99,6 @@ class SimpliSafeAlarm(alarm.AlarmControlPanel):
return return
self.simplisafe.set_state('off') self.simplisafe.set_state('off')
_LOGGER.info('SimpliSafe alarm disarming') _LOGGER.info('SimpliSafe alarm disarming')
self.update()
def alarm_arm_home(self, code=None): def alarm_arm_home(self, code=None):
"""Send arm home command.""" """Send arm home command."""
@ -112,7 +106,6 @@ class SimpliSafeAlarm(alarm.AlarmControlPanel):
return return
self.simplisafe.set_state('home') self.simplisafe.set_state('home')
_LOGGER.info('SimpliSafe alarm arming home') _LOGGER.info('SimpliSafe alarm arming home')
self.update()
def alarm_arm_away(self, code=None): def alarm_arm_away(self, code=None):
"""Send arm away command.""" """Send arm away command."""
@ -120,7 +113,6 @@ class SimpliSafeAlarm(alarm.AlarmControlPanel):
return return
self.simplisafe.set_state('away') self.simplisafe.set_state('away')
_LOGGER.info('SimpliSafe alarm arming away') _LOGGER.info('SimpliSafe alarm arming away')
self.update()
def _validate_code(self, code, state): def _validate_code(self, code, state):
"""Validate given code.""" """Validate given code."""

View File

@ -84,18 +84,15 @@ class VerisureAlarm(alarm.AlarmControlPanel):
hub.my_pages.alarm.set(code, 'DISARMED') hub.my_pages.alarm.set(code, 'DISARMED')
_LOGGER.info('verisure alarm disarming') _LOGGER.info('verisure alarm disarming')
hub.my_pages.alarm.wait_while_pending() hub.my_pages.alarm.wait_while_pending()
self.update()
def alarm_arm_home(self, code=None): def alarm_arm_home(self, code=None):
"""Send arm home command.""" """Send arm home command."""
hub.my_pages.alarm.set(code, 'ARMED_HOME') hub.my_pages.alarm.set(code, 'ARMED_HOME')
_LOGGER.info('verisure alarm arming home') _LOGGER.info('verisure alarm arming home')
hub.my_pages.alarm.wait_while_pending() hub.my_pages.alarm.wait_while_pending()
self.update()
def alarm_arm_away(self, code=None): def alarm_arm_away(self, code=None):
"""Send arm away command.""" """Send arm away command."""
hub.my_pages.alarm.set(code, 'ARMED_AWAY') hub.my_pages.alarm.set(code, 'ARMED_AWAY')
_LOGGER.info('verisure alarm arming away') _LOGGER.info('verisure alarm arming away')
hub.my_pages.alarm.wait_while_pending() hub.my_pages.alarm.wait_while_pending()
self.update()

View File

@ -4,6 +4,8 @@ Offer MQTT listening automation rules.
For more details about this automation rule, please refer to the documentation For more details about this automation rule, please refer to the documentation
at https://home-assistant.io/components/automation/#mqtt-trigger at https://home-assistant.io/components/automation/#mqtt-trigger
""" """
import json
import voluptuous as vol import voluptuous as vol
from homeassistant.core import callback from homeassistant.core import callback
@ -31,13 +33,20 @@ def async_trigger(hass, config, action):
def mqtt_automation_listener(msg_topic, msg_payload, qos): def mqtt_automation_listener(msg_topic, msg_payload, qos):
"""Listen for MQTT messages.""" """Listen for MQTT messages."""
if payload is None or payload == msg_payload: if payload is None or payload == msg_payload:
data = {
'platform': 'mqtt',
'topic': msg_topic,
'payload': msg_payload,
'qos': qos,
}
try:
data['payload_json'] = json.loads(msg_payload)
except ValueError:
pass
hass.async_run_job(action, { hass.async_run_job(action, {
'trigger': { 'trigger': data
'platform': 'mqtt',
'topic': msg_topic,
'payload': msg_payload,
'qos': qos,
}
}) })
return mqtt.async_subscribe(hass, topic, mqtt_automation_listener) return mqtt.async_subscribe(hass, topic, mqtt_automation_listener)

View File

@ -64,10 +64,19 @@ def async_trigger(hass, config, action):
call_action() call_action()
return return
@callback
def clear_listener():
"""Clear all unsub listener."""
nonlocal async_remove_state_for_cancel
nonlocal async_remove_state_for_listener
async_remove_state_for_listener = None
async_remove_state_for_cancel = None
@callback @callback
def state_for_listener(now): def state_for_listener(now):
"""Fire on state changes after a delay and calls action.""" """Fire on state changes after a delay and calls action."""
async_remove_state_for_cancel() async_remove_state_for_cancel()
clear_listener()
call_action() call_action()
@callback @callback
@ -77,6 +86,7 @@ def async_trigger(hass, config, action):
return return
async_remove_state_for_listener() async_remove_state_for_listener()
async_remove_state_for_cancel() async_remove_state_for_cancel()
clear_listener()
async_remove_state_for_listener = async_track_point_in_utc_time( async_remove_state_for_listener = async_track_point_in_utc_time(
hass, state_for_listener, dt_util.utcnow() + time_delta) hass, state_for_listener, dt_util.utcnow() + time_delta)

View File

@ -0,0 +1,257 @@
"""Contains functionality to use flic buttons as a binary sensor."""
import asyncio
import logging
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.const import (
CONF_HOST, CONF_PORT, CONF_DISCOVERY, CONF_TIMEOUT,
EVENT_HOMEASSISTANT_STOP)
from homeassistant.components.binary_sensor import (
BinarySensorDevice, PLATFORM_SCHEMA)
from homeassistant.util.async import run_callback_threadsafe
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_TYPES = [CLICK_TYPE_SINGLE, CLICK_TYPE_DOUBLE, CLICK_TYPE_HOLD]
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"
# 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_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)])
})
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
"""Setup the flic platform."""
import pyflic
# Initialize flic client responsible for
# connecting to buttons and retrieving events
host = config.get(CONF_HOST)
port = config.get(CONF_PORT)
discovery = config.get(CONF_DISCOVERY)
try:
client = pyflic.FlicClient(host, port)
except ConnectionRefusedError:
_LOGGER.error("Failed to connect to flic server.")
return
def new_button_callback(address):
"""Setup newly verified button as device in home assistant."""
hass.add_job(async_setup_button(hass, config, async_add_entities,
client, address))
client.on_new_verified_button = new_button_callback
if discovery:
start_scanning(hass, config, async_add_entities, client)
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP,
lambda event: client.close())
hass.loop.run_in_executor(None, client.handle_events)
# Get addresses of already verified buttons
addresses = yield from async_get_verified_addresses(client)
if addresses:
for address in addresses:
yield from async_setup_button(hass, config, async_add_entities,
client, address)
def start_scanning(hass, config, async_add_entities, client):
"""Start a new flic client for scanning & connceting to new buttons."""
import pyflic
scan_wizard = pyflic.ScanWizard()
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)
elif result != pyflic.ScanWizardResult.WizardFailedTimeout:
_LOGGER.warning("Failed to connect to button (%s). Reason: %s",
address, result)
# Restart scan wizard
start_scanning(hass, config, async_add_entities, client)
scan_wizard.on_completed = scan_completed_callback
client.add_scan_wizard(scan_wizard)
@asyncio.coroutine
def async_setup_button(hass, config, async_add_entities, client, address):
"""Setup single button device."""
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)
yield from async_add_entities([button])
@asyncio.coroutine
def async_get_verified_addresses(client):
"""Retrieve addresses of verified buttons."""
future = asyncio.Future()
loop = asyncio.get_event_loop()
def get_info_callback(items):
"""Set the addressed of connected buttons as result of the future."""
addresses = items["bd_addr_of_verified_buttons"]
run_callback_threadsafe(loop, future.set_result, addresses)
client.get_info(get_info_callback)
return future
class FlicButton(BinarySensorDevice):
"""Representation of a flic button."""
def __init__(self, hass, client, address, timeout, ignored_click_types):
"""Initialize the flic button."""
import pyflic
self._hass = hass
self._address = address
self._timeout = timeout
self._is_down = False
self._ignored_click_types = ignored_click_types or []
self._hass_click_types = {
pyflic.ClickType.ButtonClick: CLICK_TYPE_SINGLE,
pyflic.ClickType.ButtonSingleClick: CLICK_TYPE_SINGLE,
pyflic.ClickType.ButtonDoubleClick: CLICK_TYPE_DOUBLE,
pyflic.ClickType.ButtonHold: CLICK_TYPE_HOLD,
}
self._channel = self._create_channel()
client.add_connection_channel(self._channel)
def _create_channel(self):
"""Create a new connection channel to the button."""
import pyflic
channel = pyflic.ButtonConnectionChannel(self._address)
channel.on_button_up_or_down = self._on_up_down
# If all types of clicks should be ignored, skip registering callbacks
if set(self._ignored_click_types) == set(CLICK_TYPES):
return channel
if CLICK_TYPE_DOUBLE in self._ignored_click_types:
# Listen to all but double click type events
channel.on_button_click_or_hold = self._on_click
elif CLICK_TYPE_HOLD in self._ignored_click_types:
# Listen to all but hold click type events
channel.on_button_single_or_double_click = self._on_click
else:
# Listen to all click type events
channel.on_button_single_or_double_click_or_hold = self._on_click
return channel
@property
def name(self):
"""Return the name of the device."""
return "flic_%s" % self.address.replace(":", "")
@property
def address(self):
"""Return the bluetooth address of the device."""
return self._address
@property
def is_on(self):
"""Return true if sensor is on."""
return self._is_down
@property
def should_poll(self):
"""No polling needed."""
return False
@property
def state_attributes(self):
"""Return device specific state attributes."""
attr = super(FlicButton, self).state_attributes
attr["address"] = self.address
return attr
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")
if time_diff > self._timeout:
_LOGGER.warning(
"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.",
click_type, self.address, time_string)
return False
def _on_up_down(self, channel, click_type, was_queued, time_diff):
"""Update device state, if event was not queued."""
import pyflic
if was_queued and self._queued_event_check(click_type, time_diff):
return
self._is_down = click_type == pyflic.ClickType.ButtonDown
self.schedule_update_ha_state()
def _on_click(self, channel, click_type, was_queued, time_diff):
"""Fire click event, if event was not queued."""
# Return if click event was queued beyond allowed timeout
if was_queued and self._queued_event_check(click_type, time_diff):
return
# Return if click event is in ignored click types
hass_click_type = self._hass_click_types[click_type]
if hass_click_type in self._ignored_click_types:
return
self._hass.bus.fire(EVENT_NAME, {
EVENT_DATA_NAME: self.name,
EVENT_DATA_ADDRESS: self.address,
EVENT_DATA_QUEUED_TIME: time_diff,
EVENT_DATA_TYPE: hass_click_type
})
def _connection_status_changed(self, channel,
connection_status, disconnect_reason):
"""Remove device, if button disconnects."""
import pyflic
if connection_status == pyflic.ConnectionStatus.Disconnected:
_LOGGER.info("Button (%s) disconnected. Reason: %s",
self.address, disconnect_reason)
self.remove()

View File

@ -23,9 +23,11 @@ _LOGGER = logging.getLogger(__name__)
# These are the available sensors mapped to binary_sensor class # These are the available sensors mapped to binary_sensor class
SENSOR_TYPES = { SENSOR_TYPES = {
"Someone known": "motion", "Someone known": 'occupancy',
"Someone unknown": "motion", "Someone unknown": 'motion',
"Motion": "motion", "Motion": 'motion',
"Tag Vibration": 'vibration',
"Tag Open": 'opening',
} }
CONF_HOME = 'home' CONF_HOME = 'home'
@ -48,6 +50,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
home = config.get(CONF_HOME, None) home = config.get(CONF_HOME, None)
timeout = config.get(CONF_TIMEOUT, 15) timeout = config.get(CONF_TIMEOUT, 15)
module_name = None
import lnetatmo import lnetatmo
try: try:
data = WelcomeData(netatmo.NETATMO_AUTH, home) data = WelcomeData(netatmo.NETATMO_AUTH, home)
@ -64,23 +68,35 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
camera_name not in config[CONF_CAMERAS]: camera_name not in config[CONF_CAMERAS]:
continue continue
for variable in sensors: for variable in sensors:
add_devices([WelcomeBinarySensor(data, camera_name, home, timeout, if variable in ('Tag Vibration', 'Tag Open'):
variable)]) continue
add_devices([WelcomeBinarySensor(data, camera_name, module_name,
home, timeout, variable)])
for module_name in data.get_module_names(camera_name):
for variable in sensors:
if variable in ('Tag Vibration', 'Tag Open'):
add_devices([WelcomeBinarySensor(data, camera_name,
module_name, home,
timeout, variable)])
class WelcomeBinarySensor(BinarySensorDevice): class WelcomeBinarySensor(BinarySensorDevice):
"""Represent a single binary sensor in a Netatmo Welcome device.""" """Represent a single binary sensor in a Netatmo Welcome device."""
def __init__(self, data, camera_name, home, timeout, sensor): def __init__(self, data, camera_name, module_name, home, timeout, sensor):
"""Setup for access to the Netatmo camera events.""" """Setup for access to the Netatmo camera events."""
self._data = data self._data = data
self._camera_name = camera_name self._camera_name = camera_name
self._module_name = module_name
self._home = home self._home = home
self._timeout = timeout self._timeout = timeout
if home: if home:
self._name = home + ' / ' + camera_name self._name = home + ' / ' + camera_name
else: else:
self._name = camera_name self._name = camera_name
if module_name:
self._name += ' / ' + module_name
self._sensor_name = sensor self._sensor_name = sensor
self._name += ' ' + sensor self._name += ' ' + sensor
camera_id = data.welcomedata.cameraByName(camera=camera_name, camera_id = data.welcomedata.cameraByName(camera=camera_name,
@ -112,7 +128,7 @@ class WelcomeBinarySensor(BinarySensorDevice):
def update(self): def update(self):
"""Request an update from the Netatmo API.""" """Request an update from the Netatmo API."""
self._data.update() self._data.update()
self._data.welcomedata.updateEvent(home=self._data.home) self._data.update_event()
if self._sensor_name == "Someone known": if self._sensor_name == "Someone known":
self._state =\ self._state =\
@ -129,5 +145,16 @@ class WelcomeBinarySensor(BinarySensorDevice):
self._data.welcomedata.motionDetected(self._home, self._data.welcomedata.motionDetected(self._home,
self._camera_name, self._camera_name,
self._timeout*60) self._timeout*60)
elif self._sensor_name == "Tag Vibration":
self._state =\
self._data.welcomedata.moduleMotionDetected(self._home,
self._module_name,
self._camera_name,
self._timeout*60)
elif self._sensor_name == "Tag Open":
self._state =\
self._data.welcomedata.moduleOpened(self._home,
self._module_name,
self._camera_name)
else: else:
return None return None

View File

@ -40,6 +40,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
for sensor in pywink.get_smoke_and_co_detectors(): for sensor in pywink.get_smoke_and_co_detectors():
add_devices([WinkBinarySensorDevice(sensor, hass)]) add_devices([WinkBinarySensorDevice(sensor, hass)])
for hub in pywink.get_hubs():
add_devices([WinkHub(hub, hass)])
class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice, Entity): class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice, Entity):
"""Representation of a Wink binary sensor.""" """Representation of a Wink binary sensor."""
@ -79,3 +82,24 @@ class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice, Entity):
def sensor_class(self): def sensor_class(self):
"""Return the class of this sensor, from SENSOR_CLASSES.""" """Return the class of this sensor, from SENSOR_CLASSES."""
return SENSOR_TYPES.get(self.capability) return SENSOR_TYPES.get(self.capability)
class WinkHub(WinkDevice, BinarySensorDevice, Entity):
"""Representation of a Wink Hub."""
def __init(self, wink, hass):
"""Initialize the hub sensor."""
WinkDevice.__init__(self, wink, hass)
@property
def device_state_attributes(self):
"""Return the state attributes."""
return {
'update needed': self.wink.update_needed(),
'firmware version': self.wink.firmware_version()
}
@property
def is_on(self):
"""Return true if the binary sensor is on."""
return self.wink.state()

View File

@ -18,16 +18,26 @@ REQUIREMENTS = ['amcrest==1.0.0']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DEFAULT_PORT = 80 CONF_RESOLUTION = 'resolution'
DEFAULT_NAME = 'Amcrest Camera' DEFAULT_NAME = 'Amcrest Camera'
DEFAULT_PORT = 80
DEFAULT_RESOLUTION = 'high'
NOTIFICATION_ID = 'amcrest_notification' NOTIFICATION_ID = 'amcrest_notification'
NOTIFICATION_TITLE = 'Amcrest Camera Setup' NOTIFICATION_TITLE = 'Amcrest Camera Setup'
RESOLUTION_LIST = {
'high': 0,
'low': 1,
}
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string, vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string, vol.Required(CONF_PASSWORD): cv.string,
vol.Optional(CONF_RESOLUTION, default=DEFAULT_RESOLUTION):
vol.All(vol.In(RESOLUTION_LIST)),
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
}) })
@ -64,13 +74,14 @@ class AmcrestCam(Camera):
def __init__(self, device_info, data): def __init__(self, device_info, data):
"""Initialize an Amcrest camera.""" """Initialize an Amcrest camera."""
super(AmcrestCam, self).__init__() super(AmcrestCam, self).__init__()
self._name = device_info.get(CONF_NAME)
self._data = data self._data = data
self._name = device_info.get(CONF_NAME)
self._resolution = RESOLUTION_LIST[device_info.get(CONF_RESOLUTION)]
def camera_image(self): def camera_image(self):
"""Return a still image reponse from the camera.""" """Return a still image reponse from the camera."""
# Send the request to snap a picture and return raw jpg data # Send the request to snap a picture and return raw jpg data
response = self._data.camera.snapshot() response = self._data.camera.snapshot(channel=self._resolution)
return response.data return response.data
@property @property

View File

@ -195,8 +195,9 @@ class Thermostat(ClimateDevice):
mode = self.mode mode = self.mode
events = self.thermostat['events'] events = self.thermostat['events']
for event in events: for event in events:
if event['running']: if event['holdClimateRef'] == 'away' or \
mode = event['holdClimateRef'] event['type'] == 'autoAway':
mode = "away"
break break
return 'away' in mode return 'away' in mode

View File

@ -198,24 +198,30 @@ class GenericThermostat(ClimateDevice):
return return
if self.ac_mode: if self.ac_mode:
too_hot = self._cur_temp - self._target_temp > self._tolerance
is_cooling = self._is_device_active is_cooling = self._is_device_active
if too_hot and not is_cooling: if is_cooling:
_LOGGER.info('Turning on AC %s', self.heater_entity_id) too_cold = self._target_temp - self._cur_temp > self._tolerance
switch.turn_on(self.hass, self.heater_entity_id) if too_cold:
elif not too_hot and is_cooling: _LOGGER.info('Turning off AC %s', self.heater_entity_id)
_LOGGER.info('Turning off AC %s', self.heater_entity_id) switch.turn_off(self.hass, self.heater_entity_id)
switch.turn_off(self.hass, self.heater_entity_id) else:
too_hot = self._cur_temp - self._target_temp > self._tolerance
if too_hot:
_LOGGER.info('Turning on AC %s', self.heater_entity_id)
switch.turn_on(self.hass, self.heater_entity_id)
else: else:
too_cold = self._target_temp - self._cur_temp > self._tolerance
is_heating = self._is_device_active is_heating = self._is_device_active
if is_heating:
if too_cold and not is_heating: too_hot = self._cur_temp - self._target_temp > self._tolerance
_LOGGER.info('Turning on heater %s', self.heater_entity_id) if too_hot:
switch.turn_on(self.hass, self.heater_entity_id) _LOGGER.info('Turning off heater %s',
elif not too_cold and is_heating: self.heater_entity_id)
_LOGGER.info('Turning off heater %s', self.heater_entity_id) switch.turn_off(self.hass, self.heater_entity_id)
switch.turn_off(self.hass, self.heater_entity_id) else:
too_cold = self._target_temp - self._cur_temp > self._tolerance
if too_cold:
_LOGGER.info('Turning on heater %s', self.heater_entity_id)
switch.turn_on(self.hass, self.heater_entity_id)
@property @property
def _is_device_active(self): def _is_device_active(self):

View File

@ -155,8 +155,8 @@ class NestThermostat(ClimateDevice):
"""Set new target temperature.""" """Set new target temperature."""
target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW) target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW)
target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH) target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH)
if target_temp_low is not None and target_temp_high is not None: if self._mode == STATE_HEAT_COOL:
if self._mode == STATE_HEAT_COOL: if target_temp_low is not None and target_temp_high is not None:
temp = (target_temp_low, target_temp_high) temp = (target_temp_low, target_temp_high)
else: else:
temp = kwargs.get(ATTR_TEMPERATURE) temp = kwargs.get(ATTR_TEMPERATURE)

View File

@ -23,10 +23,19 @@ ATTR_FAN = 'fan'
ATTR_MODE = 'mode' ATTR_MODE = 'mode'
CONF_HOLD_TEMP = 'hold_temp' CONF_HOLD_TEMP = 'hold_temp'
CONF_AWAY_TEMPERATURE_HEAT = 'away_temperature_heat'
CONF_AWAY_TEMPERATURE_COOL = 'away_temperature_cool'
DEFAULT_AWAY_TEMPERATURE_HEAT = 60
DEFAULT_AWAY_TEMPERATURE_COOL = 85
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_HOST): vol.All(cv.ensure_list, [cv.string]), vol.Optional(CONF_HOST): vol.All(cv.ensure_list, [cv.string]),
vol.Optional(CONF_HOLD_TEMP, default=False): cv.boolean, vol.Optional(CONF_HOLD_TEMP, default=False): cv.boolean,
vol.Optional(CONF_AWAY_TEMPERATURE_HEAT,
default=DEFAULT_AWAY_TEMPERATURE_HEAT): vol.Coerce(float),
vol.Optional(CONF_AWAY_TEMPERATURE_COOL,
default=DEFAULT_AWAY_TEMPERATURE_COOL): vol.Coerce(float),
}) })
@ -45,12 +54,16 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
return False return False
hold_temp = config.get(CONF_HOLD_TEMP) hold_temp = config.get(CONF_HOLD_TEMP)
away_temps = [
config.get(CONF_AWAY_TEMPERATURE_HEAT),
config.get(CONF_AWAY_TEMPERATURE_COOL)
]
tstats = [] tstats = []
for host in hosts: for host in hosts:
try: try:
tstat = radiotherm.get_thermostat(host) tstat = radiotherm.get_thermostat(host)
tstats.append(RadioThermostat(tstat, hold_temp)) tstats.append(RadioThermostat(tstat, hold_temp, away_temps))
except OSError: except OSError:
_LOGGER.exception("Unable to connect to Radio Thermostat: %s", _LOGGER.exception("Unable to connect to Radio Thermostat: %s",
host) host)
@ -61,7 +74,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class RadioThermostat(ClimateDevice): class RadioThermostat(ClimateDevice):
"""Representation of a Radio Thermostat.""" """Representation of a Radio Thermostat."""
def __init__(self, device, hold_temp): def __init__(self, device, hold_temp, away_temps):
"""Initialize the thermostat.""" """Initialize the thermostat."""
self.device = device self.device = device
self.set_time() self.set_time()
@ -71,7 +84,10 @@ class RadioThermostat(ClimateDevice):
self._name = None self._name = None
self._fmode = None self._fmode = None
self._tmode = None self._tmode = None
self.hold_temp = hold_temp self._hold_temp = hold_temp
self._away = False
self._away_temps = away_temps
self._prev_temp = None
self.update() self.update()
self._operation_list = [STATE_AUTO, STATE_COOL, STATE_HEAT, STATE_OFF] self._operation_list = [STATE_AUTO, STATE_COOL, STATE_HEAT, STATE_OFF]
@ -113,6 +129,11 @@ class RadioThermostat(ClimateDevice):
"""Return the temperature we try to reach.""" """Return the temperature we try to reach."""
return self._target_temperature return self._target_temperature
@property
def is_away_mode_on(self):
"""Return true if away mode is on."""
return self._away
def update(self): def update(self):
"""Update the data from the thermostat.""" """Update the data from the thermostat."""
self._current_temperature = self.device.temp['raw'] self._current_temperature = self.device.temp['raw']
@ -138,7 +159,7 @@ class RadioThermostat(ClimateDevice):
self.device.t_cool = round(temperature * 2.0) / 2.0 self.device.t_cool = round(temperature * 2.0) / 2.0
elif self._current_operation == STATE_HEAT: elif self._current_operation == STATE_HEAT:
self.device.t_heat = round(temperature * 2.0) / 2.0 self.device.t_heat = round(temperature * 2.0) / 2.0
if self.hold_temp: if self._hold_temp or self._away:
self.device.hold = 1 self.device.hold = 1
else: else:
self.device.hold = 0 self.device.hold = 0
@ -162,3 +183,23 @@ class RadioThermostat(ClimateDevice):
self.device.t_cool = round(self._target_temperature * 2.0) / 2.0 self.device.t_cool = round(self._target_temperature * 2.0) / 2.0
elif operation_mode == STATE_HEAT: elif operation_mode == STATE_HEAT:
self.device.t_heat = round(self._target_temperature * 2.0) / 2.0 self.device.t_heat = round(self._target_temperature * 2.0) / 2.0
def turn_away_mode_on(self):
"""Turn away on.
The RTCOA app simulates away mode by using a hold.
"""
away_temp = None
if not self._away:
self._prev_temp = self._target_temperature
if self._current_operation == STATE_HEAT:
away_temp = self._away_temps[0]
elif self._current_operation == STATE_COOL:
away_temp = self._away_temps[1]
self._away = True
self.set_temperature(temperature=away_temp)
def turn_away_mode_off(self):
"""Turn away off."""
self._away = False
self.set_temperature(temperature=self._prev_temp)

View File

@ -0,0 +1,46 @@
"""
Support for Tellstick covers using Tellstick Net.
This platform uses the Telldus Live online service.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/cover.tellduslive/
"""
import logging
from homeassistant.components.cover import CoverDevice
from homeassistant.components.tellduslive import TelldusLiveEntity
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup covers."""
if discovery_info is None:
return
add_devices(TelldusLiveCover(hass, cover) for cover in discovery_info)
class TelldusLiveCover(TelldusLiveEntity, CoverDevice):
"""Representation of a cover."""
@property
def is_closed(self):
"""Return the current position of the cover."""
return self.device.is_down
def close_cover(self, **kwargs):
"""Close the cover."""
self.device.down()
self.changed()
def open_cover(self, **kwargs):
"""Open the cover."""
self.device.up()
self.changed()
def stop_cover(self, **kwargs):
"""Stop the cover."""
self.device.stop()
self.changed()

View File

@ -282,7 +282,7 @@ class DeviceTracker(object):
list(self.group.tracking) + [device.entity_id]) list(self.group.tracking) + [device.entity_id])
# lookup mac vendor string to be stored in config # lookup mac vendor string to be stored in config
device.set_vendor_for_mac() yield from device.set_vendor_for_mac()
# update known_devices.yaml # update known_devices.yaml
self.hass.async_add_job( self.hass.async_add_job(
@ -370,6 +370,7 @@ class Device(Entity):
self.away_hide = hide_if_away self.away_hide = hide_if_away
self.vendor = vendor self.vendor = vendor
self._attributes = {}
@property @property
def name(self): def name(self):
@ -399,12 +400,13 @@ class Device(Entity):
if self.battery: if self.battery:
attr[ATTR_BATTERY] = self.battery attr[ATTR_BATTERY] = self.battery
if self.attributes:
for key, value in self.attributes.items():
attr[key] = value
return attr return attr
@property
def device_state_attributes(self):
"""Return device state attributes."""
return self._attributes
@property @property
def hidden(self): def hidden(self):
"""If device should be hidden.""" """If device should be hidden."""
@ -419,8 +421,11 @@ class Device(Entity):
self.host_name = host_name self.host_name = host_name
self.location_name = location_name self.location_name = location_name
self.gps_accuracy = gps_accuracy or 0 self.gps_accuracy = gps_accuracy or 0
self.battery = battery if battery:
self.attributes = attributes self.battery = battery
if attributes:
self._attributes.update(attributes)
self.gps = None self.gps = None
if gps is not None: if gps is not None:

View File

@ -286,8 +286,10 @@ class AsusWrtDeviceScanner(object):
# match mac addresses to IP addresses in ARP table # match mac addresses to IP addresses in ARP table
for arp in result.arp: for arp in result.arp:
if match.group('mac').lower() in arp.decode('utf-8'): if match.group('mac').lower() in \
arp_match = _ARP_REGEX.search(arp.decode('utf-8')) arp.decode('utf-8').lower():
arp_match = _ARP_REGEX.search(
arp.decode('utf-8').lower())
if not arp_match: if not arp_match:
_LOGGER.warning('Could not parse arp row: %s', arp) _LOGGER.warning('Could not parse arp row: %s', arp)
continue continue

View File

@ -64,9 +64,22 @@ class GPSLoggerView(HomeAssistantView):
if 'battery' in data: if 'battery' in data:
battery = float(data['battery']) battery = float(data['battery'])
attrs = {}
if 'speed' in data:
attrs['speed'] = float(data['speed'])
if 'direction' in data:
attrs['direction'] = float(data['direction'])
if 'altitude' in data:
attrs['altitude'] = float(data['altitude'])
if 'provider' in data:
attrs['provider'] = data['provider']
if 'activity' in data:
attrs['activity'] = data['activity']
yield from hass.loop.run_in_executor( yield from hass.loop.run_in_executor(
None, partial(self.see, dev_id=device, None, partial(self.see, dev_id=device,
gps=gps_location, battery=battery, gps=gps_location, battery=battery,
gps_accuracy=accuracy)) gps_accuracy=accuracy,
attributes=attrs))
return 'Setting location for {}'.format(device) return 'Setting location for {}'.format(device)

View File

@ -25,6 +25,8 @@ _LOGGER = logging.getLogger(__name__)
CONF_EXCLUDE = 'exclude' CONF_EXCLUDE = 'exclude'
# Interval in minutes to exclude devices from a scan while they are home # Interval in minutes to exclude devices from a scan while they are home
CONF_HOME_INTERVAL = 'home_interval' CONF_HOME_INTERVAL = 'home_interval'
CONF_OPTIONS = 'scan_options'
DEFAULT_OPTIONS = '-F --host-timeout 5s'
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5) MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
@ -33,7 +35,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOSTS): cv.ensure_list, vol.Required(CONF_HOSTS): cv.ensure_list,
vol.Required(CONF_HOME_INTERVAL, default=0): cv.positive_int, vol.Required(CONF_HOME_INTERVAL, default=0): cv.positive_int,
vol.Optional(CONF_EXCLUDE, default=[]): vol.Optional(CONF_EXCLUDE, default=[]):
vol.All(cv.ensure_list, vol.Length(min=1)) vol.All(cv.ensure_list, vol.Length(min=1)),
vol.Optional(CONF_OPTIONS, default=DEFAULT_OPTIONS):
cv.string
}) })
@ -69,8 +73,9 @@ class NmapDeviceScanner(object):
self.last_results = [] self.last_results = []
self.hosts = config[CONF_HOSTS] self.hosts = config[CONF_HOSTS]
self.exclude = config.get(CONF_EXCLUDE, []) self.exclude = config[CONF_EXCLUDE]
minutes = config[CONF_HOME_INTERVAL] minutes = config[CONF_HOME_INTERVAL]
self._options = config[CONF_OPTIONS]
self.home_interval = timedelta(minutes=minutes) self.home_interval = timedelta(minutes=minutes)
self.success_init = self._update_info() self.success_init = self._update_info()
@ -103,7 +108,7 @@ class NmapDeviceScanner(object):
from nmap import PortScanner, PortScannerError from nmap import PortScanner, PortScannerError
scanner = PortScanner() scanner = PortScanner()
options = '-F --host-timeout 5s ' options = self._options
if self.home_interval: if self.home_interval:
boundary = dt_util.now() - self.home_interval boundary = dt_util.now() - self.home_interval

View File

@ -9,16 +9,20 @@ import urllib
import voluptuous as vol import voluptuous as vol
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
import homeassistant.loader as loader
from homeassistant.components.device_tracker import DOMAIN, PLATFORM_SCHEMA from homeassistant.components.device_tracker import DOMAIN, PLATFORM_SCHEMA
from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD
# Unifi package doesn't list urllib3 as a requirement # Unifi package doesn't list urllib3 as a requirement
REQUIREMENTS = ['urllib3', 'unifi==1.2.5'] REQUIREMENTS = ['urllib3', 'pyunifi==1.3']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONF_PORT = 'port' CONF_PORT = 'port'
CONF_SITE_ID = 'site_id' CONF_SITE_ID = 'site_id'
NOTIFICATION_ID = 'unifi_notification'
NOTIFICATION_TITLE = 'Unifi Device Tracker Setup'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_HOST, default='localhost'): cv.string, vol.Optional(CONF_HOST, default='localhost'): cv.string,
vol.Optional(CONF_SITE_ID, default='default'): cv.string, vol.Optional(CONF_SITE_ID, default='default'): cv.string,
@ -30,7 +34,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
def get_scanner(hass, config): def get_scanner(hass, config):
"""Setup Unifi device_tracker.""" """Setup Unifi device_tracker."""
from unifi.controller import Controller from pyunifi.controller import Controller
host = config[DOMAIN].get(CONF_HOST) host = config[DOMAIN].get(CONF_HOST)
username = config[DOMAIN].get(CONF_USERNAME) username = config[DOMAIN].get(CONF_USERNAME)
@ -38,10 +42,18 @@ def get_scanner(hass, config):
site_id = config[DOMAIN].get(CONF_SITE_ID) site_id = config[DOMAIN].get(CONF_SITE_ID)
port = config[DOMAIN].get(CONF_PORT) port = config[DOMAIN].get(CONF_PORT)
persistent_notification = loader.get_component('persistent_notification')
try: try:
ctrl = Controller(host, username, password, port, 'v4', site_id) ctrl = Controller(host, username, password, port, 'v4', site_id)
except urllib.error.HTTPError as ex: except urllib.error.HTTPError as ex:
_LOGGER.error('Failed to connect to unifi: %s', ex) _LOGGER.error('Failed to connect to Unifi: %s', ex)
persistent_notification.create(
hass, 'Failed to connect to Unifi. '
'Error: {}<br />'
'You will need to restart hass after fixing.'
''.format(ex),
title=NOTIFICATION_TITLE,
notification_id=NOTIFICATION_ID)
return False return False
return UnifiScanner(ctrl) return UnifiScanner(ctrl)

View File

@ -14,7 +14,7 @@ import voluptuous as vol
from homeassistant.const import EVENT_HOMEASSISTANT_START from homeassistant.const import EVENT_HOMEASSISTANT_START
from homeassistant.helpers.discovery import load_platform, discover from homeassistant.helpers.discovery import load_platform, discover
REQUIREMENTS = ['netdisco==0.7.7'] REQUIREMENTS = ['netdisco==0.8.0']
DOMAIN = 'discovery' DOMAIN = 'discovery'

View File

@ -6,8 +6,8 @@ from aiohttp import web
from homeassistant import core from homeassistant import core
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, SERVICE_TURN_OFF, SERVICE_TURN_ON, ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_ON,
STATE_ON, STATE_OFF, HTTP_BAD_REQUEST, HTTP_NOT_FOUND, STATE_OFF, HTTP_BAD_REQUEST, HTTP_NOT_FOUND,
) )
from homeassistant.components.light import ( from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_SUPPORTED_FEATURES, SUPPORT_BRIGHTNESS ATTR_BRIGHTNESS, ATTR_SUPPORTED_FEATURES, SUPPORT_BRIGHTNESS
@ -251,8 +251,7 @@ def entity_to_json(entity, is_on=None, brightness=None):
if brightness is None: if brightness is None:
brightness = 255 if is_on else 0 brightness = 255 if is_on else 0
name = entity.attributes.get( name = entity.attributes.get(ATTR_EMULATED_HUE_NAME, entity.name)
ATTR_EMULATED_HUE_NAME, entity.attributes[ATTR_FRIENDLY_NAME])
return { return {
'state': 'state':

View File

@ -2,17 +2,17 @@
FINGERPRINTS = { FINGERPRINTS = {
"core.js": "5dfb2d3e567fad37af0321d4b29265ed", "core.js": "5dfb2d3e567fad37af0321d4b29265ed",
"frontend.html": "6a89b74ab2b76c7d28fad2aea9444ec2", "frontend.html": "ac15b11435132aab3da592f9e7b05400",
"mdi.html": "46a76f877ac9848899b8ed382427c16f", "mdi.html": "46a76f877ac9848899b8ed382427c16f",
"micromarkdown-js.html": "93b5ec4016f0bba585521cf4d18dec1a", "micromarkdown-js.html": "93b5ec4016f0bba585521cf4d18dec1a",
"panels/ha-panel-dev-event.html": "c2d5ec676be98d4474d19f94d0262c1e", "panels/ha-panel-dev-event.html": "c2d5ec676be98d4474d19f94d0262c1e",
"panels/ha-panel-dev-info.html": "a9c07bf281fe9791fb15827ec1286825", "panels/ha-panel-dev-info.html": "a9c07bf281fe9791fb15827ec1286825",
"panels/ha-panel-dev-service.html": "b3fe49532c5c03198fafb0c6ed58b76a", "panels/ha-panel-dev-service.html": "20420e2387fd93db53c8d778097e3d59",
"panels/ha-panel-dev-state.html": "65e5f791cc467561719bf591f1386054", "panels/ha-panel-dev-state.html": "65e5f791cc467561719bf591f1386054",
"panels/ha-panel-dev-template.html": "7d744ab7f7c08b6d6ad42069989de400", "panels/ha-panel-dev-template.html": "7d744ab7f7c08b6d6ad42069989de400",
"panels/ha-panel-history.html": "efe1bcdd7733b09e55f4f965d171c295", "panels/ha-panel-history.html": "efe1bcdd7733b09e55f4f965d171c295",
"panels/ha-panel-iframe.html": "d920f0aa3c903680f2f8795e2255daab", "panels/ha-panel-iframe.html": "d920f0aa3c903680f2f8795e2255daab",
"panels/ha-panel-logbook.html": "4bc5c8370a85a4215413fbae8f85addb", "panels/ha-panel-logbook.html": "4bc5c8370a85a4215413fbae8f85addb",
"panels/ha-panel-map.html": "1bf6965b24d76db71a1871865cd4a3a2", "panels/ha-panel-map.html": "3b0ca63286cbe80f27bd36dbc2434e89",
"websocket_test.html": "575de64b431fe11c3785bf96d7813450" "websocket_test.html": "575de64b431fe11c3785bf96d7813450"
} }

File diff suppressed because one or more lines are too long

@ -1 +1 @@
Subproject commit 2652823d35b77411988751cc74820dcfc3a0e2ac Subproject commit 336e974fe6f4196aaf0b309e805a009ef0fdfd66

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

@ -29,11 +29,13 @@ ENTITY_ID_FORMAT = DOMAIN + '.{}'
CONF_ENTITIES = 'entities' CONF_ENTITIES = 'entities'
CONF_VIEW = 'view' CONF_VIEW = 'view'
CONF_CONTROL = 'control'
ATTR_AUTO = 'auto' ATTR_AUTO = 'auto'
ATTR_ORDER = 'order' ATTR_ORDER = 'order'
ATTR_VIEW = 'view' ATTR_VIEW = 'view'
ATTR_VISIBLE = 'visible' ATTR_VISIBLE = 'visible'
ATTR_CONTROL = 'control'
SERVICE_SET_VISIBILITY = 'set_visibility' SERVICE_SET_VISIBILITY = 'set_visibility'
SET_VISIBILITY_SERVICE_SCHEMA = vol.Schema({ SET_VISIBILITY_SERVICE_SCHEMA = vol.Schema({
@ -61,6 +63,7 @@ CONFIG_SCHEMA = vol.Schema({
CONF_VIEW: cv.boolean, CONF_VIEW: cv.boolean,
CONF_NAME: cv.string, CONF_NAME: cv.string,
CONF_ICON: cv.icon, CONF_ICON: cv.icon,
CONF_CONTROL: cv.string,
}, cv.match_all)) }, cv.match_all))
}, extra=vol.ALLOW_EXTRA) }, extra=vol.ALLOW_EXTRA)
@ -206,11 +209,13 @@ def _async_process_config(hass, config, component):
entity_ids = conf.get(CONF_ENTITIES) or [] entity_ids = conf.get(CONF_ENTITIES) or []
icon = conf.get(CONF_ICON) icon = conf.get(CONF_ICON)
view = conf.get(CONF_VIEW) view = conf.get(CONF_VIEW)
control = conf.get(CONF_CONTROL)
# Don't create tasks and await them all. The order is important as # Don't create tasks and await them all. The order is important as
# groups get a number based on creation order. # groups get a number based on creation order.
group = yield from Group.async_create_group( group = yield from Group.async_create_group(
hass, name, entity_ids, icon=icon, view=view, object_id=object_id) hass, name, entity_ids, icon=icon, view=view,
control=control, object_id=object_id)
groups.append(group) groups.append(group)
if groups: if groups:
@ -221,7 +226,7 @@ class Group(Entity):
"""Track a group of entity ids.""" """Track a group of entity ids."""
def __init__(self, hass, name, order=None, user_defined=True, icon=None, def __init__(self, hass, name, order=None, user_defined=True, icon=None,
view=False): view=False, control=None):
"""Initialize a group. """Initialize a group.
This Object has factory function for creation. This Object has factory function for creation.
@ -239,20 +244,22 @@ class Group(Entity):
self._assumed_state = False self._assumed_state = False
self._async_unsub_state_changed = None self._async_unsub_state_changed = None
self._visible = True self._visible = True
self._control = control
@staticmethod @staticmethod
def create_group(hass, name, entity_ids=None, user_defined=True, def create_group(hass, name, entity_ids=None, user_defined=True,
icon=None, view=False, object_id=None): icon=None, view=False, control=None, object_id=None):
"""Initialize a group.""" """Initialize a group."""
return run_coroutine_threadsafe( return run_coroutine_threadsafe(
Group.async_create_group(hass, name, entity_ids, user_defined, Group.async_create_group(hass, name, entity_ids, user_defined,
icon, view, object_id), icon, view, control, object_id),
hass.loop).result() hass.loop).result()
@staticmethod @staticmethod
@asyncio.coroutine @asyncio.coroutine
def async_create_group(hass, name, entity_ids=None, user_defined=True, def async_create_group(hass, name, entity_ids=None, user_defined=True,
icon=None, view=False, object_id=None): icon=None, view=False, control=None,
object_id=None):
"""Initialize a group. """Initialize a group.
This method must be run in the event loop. This method must be run in the event loop.
@ -260,7 +267,8 @@ class Group(Entity):
group = Group( group = Group(
hass, name, hass, name,
order=len(hass.states.async_entity_ids(DOMAIN)), order=len(hass.states.async_entity_ids(DOMAIN)),
user_defined=user_defined, icon=icon, view=view) user_defined=user_defined, icon=icon, view=view,
control=control)
group.entity_id = async_generate_entity_id( group.entity_id = async_generate_entity_id(
ENTITY_ID_FORMAT, object_id or name, hass=hass) ENTITY_ID_FORMAT, object_id or name, hass=hass)
@ -319,6 +327,8 @@ class Group(Entity):
data[ATTR_AUTO] = True data[ATTR_AUTO] = True
if self._view: if self._view:
data[ATTR_VIEW] = True data[ATTR_VIEW] = True
if self._control:
data[ATTR_CONTROL] = self._control
return data return data
@property @property

View File

@ -90,6 +90,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
if discovery_info is not None: if discovery_info is not None:
host = urlparse(discovery_info[1]).hostname host = urlparse(discovery_info[1]).hostname
if "HASS Bridge" in discovery_info[0]:
_LOGGER.info('Emulated hue found, will not add')
return False
else: else:
host = config.get(CONF_HOST, None) host = config.get(CONF_HOST, None)
@ -138,6 +142,7 @@ def setup_bridge(host, hass, add_devices, filename, allow_unreachable):
configurator.request_done(request_id) configurator.request_done(request_id)
lights = {} lights = {}
lightgroups = {}
@util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS) @util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS)
def update_lights(): def update_lights():
@ -149,9 +154,15 @@ def setup_bridge(host, hass, add_devices, filename, allow_unreachable):
_LOGGER.exception("Cannot reach the bridge") _LOGGER.exception("Cannot reach the bridge")
return return
api_states = api.get('lights') api_lights = api.get('lights')
if not isinstance(api_states, dict): if not isinstance(api_lights, dict):
_LOGGER.error("Got unexpected result from Hue API")
return
api_groups = api.get('groups')
if not isinstance(api_groups, dict):
_LOGGER.error("Got unexpected result from Hue API") _LOGGER.error("Got unexpected result from Hue API")
return return
@ -163,7 +174,7 @@ def setup_bridge(host, hass, add_devices, filename, allow_unreachable):
else: else:
bridge_type = 'hue' bridge_type = 'hue'
for light_id, info in api_states.items(): for light_id, info in api_lights.items():
if light_id not in lights: if light_id not in lights:
lights[light_id] = HueLight(int(light_id), info, lights[light_id] = HueLight(int(light_id), info,
bridge, update_lights, bridge, update_lights,
@ -171,6 +182,17 @@ def setup_bridge(host, hass, add_devices, filename, allow_unreachable):
new_lights.append(lights[light_id]) new_lights.append(lights[light_id])
else: else:
lights[light_id].info = info lights[light_id].info = info
lights[light_id].schedule_update_ha_state()
for lightgroup_id, info in api_groups.items():
if lightgroup_id not in lightgroups:
lightgroups[lightgroup_id] = HueLight(
int(lightgroup_id), info, bridge, update_lights,
bridge_type, allow_unreachable, True)
new_lights.append(lightgroups[lightgroup_id])
else:
lightgroups[lightgroup_id].info = info
lightgroups[lightgroup_id].schedule_update_ha_state()
if new_lights: if new_lights:
add_devices(new_lights) add_devices(new_lights)
@ -225,15 +247,20 @@ class HueLight(Light):
"""Representation of a Hue light.""" """Representation of a Hue light."""
def __init__(self, light_id, info, bridge, update_lights, def __init__(self, light_id, info, bridge, update_lights,
bridge_type, allow_unreachable): bridge_type, allow_unreachable, is_group=False):
"""Initialize the light.""" """Initialize the light."""
self.light_id = light_id self.light_id = light_id
self.info = info self.info = info
self.bridge = bridge self.bridge = bridge
self.update_lights = update_lights self.update_lights = update_lights
self.bridge_type = bridge_type self.bridge_type = bridge_type
self.allow_unreachable = allow_unreachable self.allow_unreachable = allow_unreachable
self.is_group = is_group
if is_group:
self._command_func = self.bridge.set_group
else:
self._command_func = self.bridge.set_light
@property @property
def unique_id(self): def unique_id(self):
@ -243,33 +270,44 @@ class HueLight(Light):
@property @property
def name(self): def name(self):
"""Return the mame of the Hue light.""" """Return the name of the Hue light."""
return self.info.get('name', DEVICE_DEFAULT_NAME) return self.info.get('name', DEVICE_DEFAULT_NAME)
@property @property
def brightness(self): def brightness(self):
"""Return the brightness of this light between 0..255.""" """Return the brightness of this light between 0..255."""
return self.info['state'].get('bri') if self.is_group:
return self.info['action'].get('bri')
else:
return self.info['state'].get('bri')
@property @property
def xy_color(self): def xy_color(self):
"""Return the XY color value.""" """Return the XY color value."""
return self.info['state'].get('xy') if self.is_group:
return self.info['action'].get('xy')
else:
return self.info['state'].get('xy')
@property @property
def color_temp(self): def color_temp(self):
"""Return the CT color value.""" """Return the CT color value."""
return self.info['state'].get('ct') if self.is_group:
return self.info['action'].get('ct')
else:
return self.info['state'].get('ct')
@property @property
def is_on(self): def is_on(self):
"""Return true if device is on.""" """Return true if device is on."""
self.update_lights() if self.is_group:
return self.info['state']['any_on']
if self.allow_unreachable:
return self.info['state']['on']
else: else:
return self.info['state']['reachable'] and self.info['state']['on'] if self.allow_unreachable:
return self.info['state']['on']
else:
return self.info['state']['reachable'] and \
self.info['state']['on']
@property @property
def supported_features(self): def supported_features(self):
@ -318,7 +356,7 @@ class HueLight(Light):
elif self.bridge_type == 'hue': elif self.bridge_type == 'hue':
command['effect'] = 'none' command['effect'] = 'none'
self.bridge.set_light(self.light_id, command) self._command_func(self.light_id, command)
def turn_off(self, **kwargs): def turn_off(self, **kwargs):
"""Turn the specified or all lights off.""" """Turn the specified or all lights off."""
@ -340,7 +378,7 @@ class HueLight(Light):
elif self.bridge_type == 'hue': elif self.bridge_type == 'hue':
command['alert'] = 'none' command['alert'] = 'none'
self.bridge.set_light(self.light_id, command) self._command_func(self.light_id, command)
def update(self): def update(self):
"""Synchronize state with bridge.""" """Synchronize state with bridge."""

View File

@ -0,0 +1,59 @@
"""
Support for Tellstick switches using Tellstick Net.
This platform uses the Telldus Live online service.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/light.tellduslive/
"""
import logging
from homeassistant.components.light import (
ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light)
from homeassistant.components.tellduslive import TelldusLiveEntity
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup lights."""
if discovery_info is None:
return
add_devices(TelldusLiveLight(hass, light) for light in discovery_info)
class TelldusLiveLight(TelldusLiveEntity, Light):
"""Representation of a light."""
def changed(self):
"""A property of the device might have changed."""
# pylint: disable=attribute-defined-outside-init
self._last_brightness = self.brightness
super().changed()
@property
def brightness(self):
"""Return the brightness of this light between 0..255."""
return self.device.dim_level
@property
def supported_features(self):
"""Flag supported features."""
return SUPPORT_BRIGHTNESS
@property
def is_on(self):
"""Return true if light is on."""
return self.device.is_on
def turn_on(self, **kwargs):
"""Turn the light on."""
brightness = kwargs.get(ATTR_BRIGHTNESS, self._last_brightness)
self.device.dim(level=brightness)
self.changed()
def turn_off(self, **kwargs):
"""Turn the light off."""
self.device.turn_off()
self.changed()

View File

@ -29,16 +29,17 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
signal_repetitions = discovery_info.get(ATTR_DISCOVER_CONFIG, signal_repetitions = discovery_info.get(ATTR_DISCOVER_CONFIG,
DEFAULT_SIGNAL_REPETITIONS) DEFAULT_SIGNAL_REPETITIONS)
add_devices(TellstickLight(tellcore_id, signal_repetitions) add_devices(TellstickLight(tellcore_id, hass.data['tellcore_registry'],
signal_repetitions)
for tellcore_id in discovery_info[ATTR_DISCOVER_DEVICES]) for tellcore_id in discovery_info[ATTR_DISCOVER_DEVICES])
class TellstickLight(TellstickDevice, Light): class TellstickLight(TellstickDevice, Light):
"""Representation of a Tellstick light.""" """Representation of a Tellstick light."""
def __init__(self, tellcore_id, signal_repetitions): def __init__(self, tellcore_id, tellcore_registry, signal_repetitions):
"""Initialize the light.""" """Initialize the light."""
super().__init__(tellcore_id, signal_repetitions) super().__init__(tellcore_id, tellcore_registry, signal_repetitions)
self._brightness = 255 self._brightness = 255
@ -71,7 +72,11 @@ class TellstickLight(TellstickDevice, Light):
if brightness is not None: if brightness is not None:
self._brightness = brightness self._brightness = brightness
self._state = (self._brightness > 0) # _brightness is not defined when called from super
try:
self._state = (self._brightness > 0)
except AttributeError:
self._state = True
else: else:
self._state = False self._state = False

View File

@ -0,0 +1,161 @@
"""
Support for interface with an Aquos TV.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/media_player.aquostv/
"""
import logging
import voluptuous as vol
from homeassistant.components.media_player import (
SUPPORT_TURN_ON, SUPPORT_TURN_OFF,
SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP,
SUPPORT_VOLUME_SET, MediaPlayerDevice, PLATFORM_SCHEMA)
from homeassistant.const import (
CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON, STATE_UNKNOWN,
CONF_PORT, CONF_USERNAME, CONF_PASSWORD)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['sharp-aquos-rc==0.2']
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = 'Sharp Aquos TV'
DEFAULT_PORT = 10002
DEFAULT_USERNAME = 'admin'
DEFAULT_PASSWORD = 'password'
SUPPORT_SHARPTV = SUPPORT_VOLUME_STEP | \
SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
SUPPORT_TURN_OFF | SUPPORT_TURN_ON
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string,
vol.Optional(CONF_PASSWORD, default=DEFAULT_PASSWORD): cv.string,
})
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Sharp Aquos TV platform."""
import sharp_aquos_rc
name = config.get(CONF_NAME)
port = config.get(CONF_PORT)
username = config.get(CONF_USERNAME)
password = config.get(CONF_PASSWORD)
if discovery_info:
_LOGGER.debug('%s', discovery_info)
vals = discovery_info.split(':')
if len(vals) > 1:
port = vals[1]
host = vals[0]
remote = sharp_aquos_rc.TV(host,
port,
username,
password)
add_devices([SharpAquosTVDevice(name, remote)])
return True
host = config.get(CONF_HOST)
remote = sharp_aquos_rc.TV(host,
port,
username,
password)
add_devices([SharpAquosTVDevice(name, remote)])
return True
# pylint: disable=abstract-method
class SharpAquosTVDevice(MediaPlayerDevice):
"""Representation of a Aquos TV."""
# pylint: disable=too-many-public-methods
def __init__(self, name, remote):
"""Initialize the aquos device."""
# Save a reference to the imported class
self._name = name
# Assume that the TV is not muted
self._muted = False
# Assume that the TV is in Play mode
self._playing = True
self._state = STATE_UNKNOWN
self._remote = remote
self._volume = 0
def update(self):
"""Retrieve the latest data."""
try:
if self._remote.power() == 1:
self._state = STATE_ON
else:
self._state = STATE_OFF
# Set TV to be able to remotely power on
# self._remote.power_on_command_settings(2)
if self._remote.mute() == 2:
self._muted = False
else:
self._muted = True
self._volume = self._remote.volume() / 60
except OSError:
self._state = STATE_OFF
@property
def name(self):
"""Return the name of the device."""
return self._name
@property
def state(self):
"""Return the state of the device."""
return self._state
@property
def volume_level(self):
"""Volume level of the media player (0..1)."""
return self._volume
@property
def is_volume_muted(self):
"""Boolean if volume is currently muted."""
return self._muted
@property
def supported_media_commands(self):
"""Flag of media commands that are supported."""
return SUPPORT_SHARPTV
def turn_off(self):
"""Turn off tvplayer."""
self._remote.power(0)
def volume_up(self):
"""Volume up the media player."""
self._remote.volume(int(self._volume * 60) + 2)
def volume_down(self):
"""Volume down media player."""
self._remote.volume(int(self._volume * 60) - 2)
def set_volume_level(self, level):
"""Set Volume media player."""
self._remote.volume(int(level * 60))
def mute_volume(self, mute):
"""Send mute command."""
self._remote.mute(0)
def turn_on(self):
"""Turn the media player on."""
self._remote.power(1)

View File

@ -18,6 +18,7 @@ from homeassistant.const import (
CONF_HOST, STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING, CONF_HOST, STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING,
STATE_UNKNOWN) STATE_UNKNOWN)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
import homeassistant.util.dt as dt_util
REQUIREMENTS = ['pychromecast==0.7.6'] REQUIREMENTS = ['pychromecast==0.7.6']
@ -105,6 +106,7 @@ class CastDevice(MediaPlayerDevice):
self.cast_status = self.cast.status self.cast_status = self.cast.status
self.media_status = self.cast.media_controller.status self.media_status = self.cast.media_controller.status
self.media_status_received = None
@property @property
def should_poll(self): def should_poll(self):
@ -231,6 +233,30 @@ class CastDevice(MediaPlayerDevice):
"""Flag of media commands that are supported.""" """Flag of media commands that are supported."""
return SUPPORT_CAST return SUPPORT_CAST
@property
def media_position(self):
"""Position of current playing media in seconds."""
if self.media_status is None or not (
self.media_status.player_is_playing or
self.media_status.player_is_idle):
return None
position = self.media_status.current_time
if self.media_status.player_is_playing:
position += (dt_util.utcnow() -
self.media_status_received).total_seconds()
return position
@property
def media_position_updated_at(self):
"""When was the position of the current playing media valid.
Returns value from homeassistant.util.dt.utcnow().
"""
return self.media_status_received
def turn_on(self): def turn_on(self):
"""Turn on the ChromeCast.""" """Turn on the ChromeCast."""
# The only way we can turn the Chromecast is on is by launching an app # The only way we can turn the Chromecast is on is by launching an app
@ -292,4 +318,5 @@ class CastDevice(MediaPlayerDevice):
def new_media_status(self, status): def new_media_status(self, status):
"""Called when a new media status is received.""" """Called when a new media status is received."""
self.media_status = status self.media_status = status
self.media_status_received = dt_util.utcnow()
self.schedule_update_ha_state() self.schedule_update_ha_state()

View File

@ -10,6 +10,7 @@ from homeassistant.components.media_player import (
SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
SUPPORT_SELECT_SOURCE, SUPPORT_CLEAR_PLAYLIST, MediaPlayerDevice) SUPPORT_SELECT_SOURCE, SUPPORT_CLEAR_PLAYLIST, MediaPlayerDevice)
from homeassistant.const import STATE_OFF, STATE_PAUSED, STATE_PLAYING from homeassistant.const import STATE_OFF, STATE_PAUSED, STATE_PLAYING
import homeassistant.util.dt as dt_util
# pylint: disable=unused-argument # pylint: disable=unused-argument
@ -18,8 +19,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
add_devices([ add_devices([
DemoYoutubePlayer( DemoYoutubePlayer(
'Living Room', 'eyU3bRy2x44', 'Living Room', 'eyU3bRy2x44',
'♥♥ The Best Fireplace Video (3 hours)'), '♥♥ The Best Fireplace Video (3 hours)', 300),
DemoYoutubePlayer('Bedroom', 'kxopViU98Xo', 'Epic sax guy 10 hours'), DemoYoutubePlayer('Bedroom', 'kxopViU98Xo', 'Epic sax guy 10 hours',
360000),
DemoMusicPlayer(), DemoTVShowPlayer(), DemoMusicPlayer(), DemoTVShowPlayer(),
]) ])
@ -78,32 +80,32 @@ class AbstractDemoPlayer(MediaPlayerDevice):
def turn_on(self): def turn_on(self):
"""Turn the media player on.""" """Turn the media player on."""
self._player_state = STATE_PLAYING self._player_state = STATE_PLAYING
self.update_ha_state() self.schedule_update_ha_state()
def turn_off(self): def turn_off(self):
"""Turn the media player off.""" """Turn the media player off."""
self._player_state = STATE_OFF self._player_state = STATE_OFF
self.update_ha_state() self.schedule_update_ha_state()
def mute_volume(self, mute): def mute_volume(self, mute):
"""Mute the volume.""" """Mute the volume."""
self._volume_muted = mute self._volume_muted = mute
self.update_ha_state() self.schedule_update_ha_state()
def set_volume_level(self, volume): def set_volume_level(self, volume):
"""Set the volume level, range 0..1.""" """Set the volume level, range 0..1."""
self._volume_level = volume self._volume_level = volume
self.update_ha_state() self.schedule_update_ha_state()
def media_play(self): def media_play(self):
"""Send play command.""" """Send play command."""
self._player_state = STATE_PLAYING self._player_state = STATE_PLAYING
self.update_ha_state() self.schedule_update_ha_state()
def media_pause(self): def media_pause(self):
"""Send pause command.""" """Send pause command."""
self._player_state = STATE_PAUSED self._player_state = STATE_PAUSED
self.update_ha_state() self.schedule_update_ha_state()
class DemoYoutubePlayer(AbstractDemoPlayer): class DemoYoutubePlayer(AbstractDemoPlayer):
@ -111,11 +113,14 @@ class DemoYoutubePlayer(AbstractDemoPlayer):
# We only implement the methods that we support # We only implement the methods that we support
def __init__(self, name, youtube_id=None, media_title=None): def __init__(self, name, youtube_id=None, media_title=None, duration=360):
"""Initialize the demo device.""" """Initialize the demo device."""
super().__init__(name) super().__init__(name)
self.youtube_id = youtube_id self.youtube_id = youtube_id
self._media_title = media_title self._media_title = media_title
self._duration = duration
self._progress = int(duration * .15)
self._progress_updated_at = dt_util.utcnow()
@property @property
def media_content_id(self): def media_content_id(self):
@ -130,7 +135,7 @@ class DemoYoutubePlayer(AbstractDemoPlayer):
@property @property
def media_duration(self): def media_duration(self):
"""Return the duration of current playing media in seconds.""" """Return the duration of current playing media in seconds."""
return 360 return self._duration
@property @property
def media_image_url(self): def media_image_url(self):
@ -152,10 +157,39 @@ class DemoYoutubePlayer(AbstractDemoPlayer):
"""Flag of media commands that are supported.""" """Flag of media commands that are supported."""
return YOUTUBE_PLAYER_SUPPORT return YOUTUBE_PLAYER_SUPPORT
@property
def media_position(self):
"""Position of current playing media in seconds."""
if self._progress is None:
return None
position = self._progress
if self._player_state == STATE_PLAYING:
position += (dt_util.utcnow() -
self._progress_updated_at).total_seconds()
return position
@property
def media_position_updated_at(self):
"""When was the position of the current playing media valid.
Returns value from homeassistant.util.dt.utcnow().
"""
if self._player_state == STATE_PLAYING:
return self._progress_updated_at
def play_media(self, media_type, media_id, **kwargs): def play_media(self, media_type, media_id, **kwargs):
"""Play a piece of media.""" """Play a piece of media."""
self.youtube_id = media_id self.youtube_id = media_id
self.update_ha_state() self.schedule_update_ha_state()
def media_pause(self):
"""Send pause command."""
self._progress = self.media_position
self._progress_updated_at = dt_util.utcnow()
super().media_pause()
class DemoMusicPlayer(AbstractDemoPlayer): class DemoMusicPlayer(AbstractDemoPlayer):
@ -249,20 +283,20 @@ class DemoMusicPlayer(AbstractDemoPlayer):
"""Send previous track command.""" """Send previous track command."""
if self._cur_track > 0: if self._cur_track > 0:
self._cur_track -= 1 self._cur_track -= 1
self.update_ha_state() self.schedule_update_ha_state()
def media_next_track(self): def media_next_track(self):
"""Send next track command.""" """Send next track command."""
if self._cur_track < len(self.tracks) - 1: if self._cur_track < len(self.tracks) - 1:
self._cur_track += 1 self._cur_track += 1
self.update_ha_state() self.schedule_update_ha_state()
def clear_playlist(self): def clear_playlist(self):
"""Clear players playlist.""" """Clear players playlist."""
self.tracks = [] self.tracks = []
self._cur_track = 0 self._cur_track = 0
self._player_state = STATE_OFF self._player_state = STATE_OFF
self.update_ha_state() self.schedule_update_ha_state()
class DemoTVShowPlayer(AbstractDemoPlayer): class DemoTVShowPlayer(AbstractDemoPlayer):
@ -344,15 +378,15 @@ class DemoTVShowPlayer(AbstractDemoPlayer):
"""Send previous track command.""" """Send previous track command."""
if self._cur_episode > 1: if self._cur_episode > 1:
self._cur_episode -= 1 self._cur_episode -= 1
self.update_ha_state() self.schedule_update_ha_state()
def media_next_track(self): def media_next_track(self):
"""Send next track command.""" """Send next track command."""
if self._cur_episode < self._episode_count: if self._cur_episode < self._episode_count:
self._cur_episode += 1 self._cur_episode += 1
self.update_ha_state() self.schedule_update_ha_state()
def select_source(self, source): def select_source(self, source):
"""Set the input source.""" """Set the input source."""
self._source = source self._source = source
self.update_ha_state() self.schedule_update_ha_state()

View File

@ -13,7 +13,7 @@ from homeassistant.components.media_player import (
PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_SELECT_SOURCE, PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_SELECT_SOURCE,
SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, SUPPORT_TURN_OFF, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, SUPPORT_TURN_OFF,
SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
MediaPlayerDevice) SUPPORT_STOP, MediaPlayerDevice)
from homeassistant.const import ( from homeassistant.const import (
CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON, STATE_UNKNOWN) CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON, STATE_UNKNOWN)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
@ -22,16 +22,32 @@ _LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = 'Music station' DEFAULT_NAME = 'Music station'
SUPPORT_DENON = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | \ SUPPORT_DENON = SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
SUPPORT_VOLUME_MUTE | SUPPORT_PREVIOUS_TRACK | \ SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE \
SUPPORT_SELECT_SOURCE | SUPPORT_NEXT_TRACK | \
SUPPORT_TURN_ON | SUPPORT_TURN_OFF SUPPORT_MEDIA_MODES = SUPPORT_PAUSE | SUPPORT_STOP | \
SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string, vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
}) })
NORMAL_INPUTS = {'Cd': 'CD', 'Dvd': 'DVD', 'Blue ray': 'BD', 'TV': 'TV',
'Satelite / Cable': 'SAT/CBL', 'Game': 'GAME',
'Game2': 'GAME2', 'Video Aux': 'V.AUX', 'Dock': 'DOCK'}
MEDIA_MODES = {'Tuner': 'TUNER', 'Media server': 'SERVER',
'Ipod dock': 'IPOD', 'Net/USB': 'NET/USB',
'Rapsody': 'RHAPSODY', 'Napster': 'NAPSTER',
'Pandora': 'PANDORA', 'LastFM': 'LASTFM',
'Flickr': 'FLICKR', 'Favorites': 'FAVORITES',
'Internet Radio': 'IRADIO', 'USB/IPOD': 'USB/IPOD'}
# Sub-modes of 'NET/USB'
# {'USB': 'USB', 'iPod Direct': 'IPD', 'Internet Radio': 'IRP',
# 'Favorites': 'FVP'}
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Denon platform.""" """Setup the Denon platform."""
@ -53,14 +69,39 @@ class DenonDevice(MediaPlayerDevice):
self._host = host self._host = host
self._pwstate = 'PWSTANDBY' self._pwstate = 'PWSTANDBY'
self._volume = 0 self._volume = 0
self._source_list = {'TV': 'SITV', 'Tuner': 'SITUNER', # Initial value 60dB, changed if we get a MVMAX
'Internet Radio': 'SIIRP', 'Favorites': 'SIFVP'} self._volume_max = 60
self._source_list = NORMAL_INPUTS.copy()
self._source_list.update(MEDIA_MODES)
self._muted = False self._muted = False
self._mediasource = '' self._mediasource = ''
self._mediainfo = ''
self._should_setup_sources = True
def _setup_sources(self, telnet):
# NSFRN - Network name
self._name = self.telnet_request(telnet, 'NSFRN ?')[len('NSFRN '):]
# SSFUN - Configured sources with names
self._source_list = {}
for line in self.telnet_request(telnet, 'SSFUN ?', all_lines=True):
source, configured_name = line[len('SSFUN'):].split(" ", 1)
self._source_list[configured_name] = source
# SSSOD - Deleted sources
for line in self.telnet_request(telnet, 'SSSOD ?', all_lines=True):
source, status = line[len('SSSOD'):].split(" ", 1)
if status == 'DEL':
for pretty_name, name in self._source_list.items():
if source == name:
del self._source_list[pretty_name]
break
@classmethod @classmethod
def telnet_request(cls, telnet, command): def telnet_request(cls, telnet, command, all_lines=False):
"""Execute `command` and return the response.""" """Execute `command` and return the response."""
_LOGGER.debug('Sending: "%s"', command)
telnet.write(command.encode('ASCII') + b'\r') telnet.write(command.encode('ASCII') + b'\r')
lines = [] lines = []
while True: while True:
@ -68,12 +109,16 @@ class DenonDevice(MediaPlayerDevice):
if not line: if not line:
break break
lines.append(line.decode('ASCII').strip()) lines.append(line.decode('ASCII').strip())
_LOGGER.debug('Recived: "%s"', line)
if all_lines:
return lines
return lines[0] return lines[0]
def telnet_command(self, command): def telnet_command(self, command):
"""Establish a telnet connection and sends `command`.""" """Establish a telnet connection and sends `command`."""
telnet = telnetlib.Telnet(self._host) telnet = telnetlib.Telnet(self._host)
_LOGGER.debug('Sending: "%s"', command)
telnet.write(command.encode('ASCII') + b'\r') telnet.write(command.encode('ASCII') + b'\r')
telnet.read_very_eager() # skip response telnet.read_very_eager() # skip response
telnet.close() telnet.close()
@ -85,12 +130,30 @@ class DenonDevice(MediaPlayerDevice):
except OSError: except OSError:
return False return False
if self._should_setup_sources:
self._setup_sources(telnet)
self._should_setup_sources = False
self._pwstate = self.telnet_request(telnet, 'PW?') self._pwstate = self.telnet_request(telnet, 'PW?')
volume_str = self.telnet_request(telnet, 'MV?')[len('MV'):] for line in self.telnet_request(telnet, 'MV?', all_lines=True):
self._volume = int(volume_str) / 60 if line.startswith('MVMAX '):
# only grab two digit max, don't care about any half digit
self._volume_max = int(line[len('MVMAX '):len('MVMAX XX')])
continue
if line.startswith('MV'):
self._volume = int(line[len('MV'):])
self._muted = (self.telnet_request(telnet, 'MU?') == 'MUON') self._muted = (self.telnet_request(telnet, 'MU?') == 'MUON')
self._mediasource = self.telnet_request(telnet, 'SI?')[len('SI'):] self._mediasource = self.telnet_request(telnet, 'SI?')[len('SI'):]
if self._mediasource in MEDIA_MODES.values():
self._mediainfo = ""
answer_codes = ["NSE0", "NSE1X", "NSE2X", "NSE3X", "NSE4", "NSE5",
"NSE6", "NSE7", "NSE8"]
for line in self.telnet_request(telnet, 'NSE', all_lines=True):
self._mediainfo += line[len(answer_codes.pop()):] + '\n'
else:
self._mediainfo = self.source
telnet.close() telnet.close()
return True return True
@ -112,7 +175,7 @@ class DenonDevice(MediaPlayerDevice):
@property @property
def volume_level(self): def volume_level(self):
"""Volume level of the media player (0..1).""" """Volume level of the media player (0..1)."""
return self._volume return self._volume / self._volume_max
@property @property
def is_volume_muted(self): def is_volume_muted(self):
@ -122,17 +185,27 @@ class DenonDevice(MediaPlayerDevice):
@property @property
def source_list(self): def source_list(self):
"""List of available input sources.""" """List of available input sources."""
return list(self._source_list.keys()) return sorted(list(self._source_list.keys()))
@property @property
def media_title(self): def media_title(self):
"""Current media source.""" """Current media info."""
return self._mediasource return self._mediainfo
@property @property
def supported_media_commands(self): def supported_media_commands(self):
"""Flag of media commands that are supported.""" """Flag of media commands that are supported."""
return SUPPORT_DENON if self._mediasource in MEDIA_MODES.values():
return SUPPORT_DENON | SUPPORT_MEDIA_MODES
else:
return SUPPORT_DENON
@property
def source(self):
"""Return the current input source."""
for pretty_name, name in self._source_list.items():
if self._mediasource == name:
return pretty_name
def turn_off(self): def turn_off(self):
"""Turn off media player.""" """Turn off media player."""
@ -148,8 +221,8 @@ class DenonDevice(MediaPlayerDevice):
def set_volume_level(self, volume): def set_volume_level(self, volume):
"""Set volume level, range 0..1.""" """Set volume level, range 0..1."""
# 60dB max self.telnet_command('MV' +
self.telnet_command('MV' + str(round(volume * 60)).zfill(2)) str(round(volume * self._volume_max)).zfill(2))
def mute_volume(self, mute): def mute_volume(self, mute):
"""Mute (true) or unmute (false) media player.""" """Mute (true) or unmute (false) media player."""
@ -163,6 +236,10 @@ class DenonDevice(MediaPlayerDevice):
"""Pause media player.""" """Pause media player."""
self.telnet_command('NS9B') self.telnet_command('NS9B')
def media_stop(self):
"""Pause media player."""
self.telnet_command('NS9C')
def media_next_track(self): def media_next_track(self):
"""Send the next track command.""" """Send the next track command."""
self.telnet_command('NS9D') self.telnet_command('NS9D')
@ -177,4 +254,4 @@ class DenonDevice(MediaPlayerDevice):
def select_source(self, source): def select_source(self, source):
"""Select input source.""" """Select input source."""
self.telnet_command(self._source_list.get(source)) self.telnet_command('SI' + self._source_list.get(source))

View File

@ -20,12 +20,15 @@ from homeassistant.const import (
STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING, STATE_UNKNOWN) STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING, STATE_UNKNOWN)
from homeassistant.helpers.event import (track_utc_time_change) from homeassistant.helpers.event import (track_utc_time_change)
from homeassistant.util import Throttle from homeassistant.util import Throttle
import homeassistant.util.dt as dt_util
REQUIREMENTS = ['pyemby==0.1'] REQUIREMENTS = ['pyemby==0.2']
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1) MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1)
MEDIA_TYPE_TRAILER = 'trailer'
DEFAULT_PORT = 8096 DEFAULT_PORT = 8096
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -119,6 +122,8 @@ class EmbyClient(MediaPlayerDevice):
self.update_sessions = update_sessions self.update_sessions = update_sessions
self.client = client self.client = client
self.set_device(device) self.set_device(device)
self.media_status_last_position = None
self.media_status_received = None
def set_device(self, device): def set_device(self, device):
"""Set the device property.""" """Set the device property."""
@ -178,6 +183,17 @@ class EmbyClient(MediaPlayerDevice):
"""Get the latest details.""" """Get the latest details."""
self.update_devices(no_throttle=True) self.update_devices(no_throttle=True)
self.update_sessions(no_throttle=True) self.update_sessions(no_throttle=True)
# Check if we should update progress
try:
position = self.session['PlayState']['PositionTicks']
except (KeyError, TypeError):
self.media_status_last_position = None
self.media_status_received = None
else:
position = int(position) / 10000000
if position != self.media_status_last_position:
self.media_status_last_position = position
self.media_status_received = dt_util.utcnow()
def play_percent(self): def play_percent(self):
"""Return current media percent complete.""" """Return current media percent complete."""
@ -220,6 +236,8 @@ class EmbyClient(MediaPlayerDevice):
return MEDIA_TYPE_TVSHOW return MEDIA_TYPE_TVSHOW
elif media_type == 'Movie': elif media_type == 'Movie':
return MEDIA_TYPE_VIDEO return MEDIA_TYPE_VIDEO
elif media_type == 'Trailer':
return MEDIA_TYPE_TRAILER
return None return None
except KeyError: except KeyError:
return None return None
@ -233,19 +251,32 @@ class EmbyClient(MediaPlayerDevice):
except KeyError: except KeyError:
return None return None
@property
def media_position(self):
"""Position of current playing media in seconds."""
return self.media_status_last_position
@property
def media_position_updated_at(self):
"""
When was the position of the current playing media valid.
Returns value from homeassistant.util.dt.utcnow().
"""
return self.media_status_received
@property @property
def media_image_url(self): def media_image_url(self):
"""Image url of current playing media.""" """Image url of current playing media."""
if self.now_playing_item is not None: if self.now_playing_item is not None:
try: try:
return self.client.get_image( return self.client.get_image(
self.now_playing_item['ThumbItemId'], 'Thumb', self.now_playing_item['ThumbItemId'], 'Thumb', 0)
self.play_percent())
except KeyError: except KeyError:
try: try:
return self.client.get_image( return self.client.get_image(
self.now_playing_item['PrimaryImageItemId'], 'Primary', self.now_playing_item[
self.play_percent()) 'PrimaryImageItemId'], 'Primary', 0)
except KeyError: except KeyError:
return None return None

View File

@ -89,7 +89,7 @@ class OnkyoDevice(MediaPlayerDevice):
except (ValueError, OSError, AttributeError, AssertionError): except (ValueError, OSError, AttributeError, AssertionError):
if self._receiver.command_socket: if self._receiver.command_socket:
self._receiver.command_socket = None self._receiver.command_socket = None
_LOGGER.info('Reseting connection to %s.', self._name) _LOGGER.info('Resetting connection to %s.', self._name)
else: else:
_LOGGER.info('%s is disconnected. Attempting to reconnect.', _LOGGER.info('%s is disconnected. Attempting to reconnect.',
self._name) self._name)
@ -173,7 +173,7 @@ class OnkyoDevice(MediaPlayerDevice):
def turn_on(self): def turn_on(self):
"""Turn the media player on.""" """Turn the media player on."""
self._receiver.power_on() self.command('system-power on')
def select_source(self, source): def select_source(self, source):
"""Set the input source.""" """Set the input source."""

View File

@ -15,7 +15,7 @@ from homeassistant.components.media_player import (
ATTR_MEDIA_ENQUEUE, DOMAIN, MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK, ATTR_MEDIA_ENQUEUE, DOMAIN, MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK,
SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK, SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK,
SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_CLEAR_PLAYLIST, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_CLEAR_PLAYLIST,
SUPPORT_SELECT_SOURCE, MediaPlayerDevice, PLATFORM_SCHEMA) SUPPORT_SELECT_SOURCE, MediaPlayerDevice, PLATFORM_SCHEMA, SUPPORT_STOP)
from homeassistant.const import ( from homeassistant.const import (
STATE_IDLE, STATE_PAUSED, STATE_PLAYING, STATE_OFF, ATTR_ENTITY_ID, STATE_IDLE, STATE_PAUSED, STATE_PLAYING, STATE_OFF, ATTR_ENTITY_ID,
CONF_HOSTS) CONF_HOSTS)
@ -36,9 +36,10 @@ _SOCO_LOGGER.setLevel(logging.ERROR)
_REQUESTS_LOGGER = logging.getLogger('requests') _REQUESTS_LOGGER = logging.getLogger('requests')
_REQUESTS_LOGGER.setLevel(logging.ERROR) _REQUESTS_LOGGER.setLevel(logging.ERROR)
SUPPORT_SONOS = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE |\ SUPPORT_SONOS = SUPPORT_STOP | SUPPORT_PAUSE | SUPPORT_VOLUME_SET |\
SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_PLAY_MEDIA |\ SUPPORT_VOLUME_MUTE | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK |\
SUPPORT_SEEK | SUPPORT_CLEAR_PLAYLIST | SUPPORT_SELECT_SOURCE SUPPORT_PLAY_MEDIA | SUPPORT_SEEK | SUPPORT_CLEAR_PLAYLIST |\
SUPPORT_SELECT_SOURCE
SERVICE_GROUP_PLAYERS = 'sonos_group_players' SERVICE_GROUP_PLAYERS = 'sonos_group_players'
SERVICE_UNJOIN = 'sonos_unjoin' SERVICE_UNJOIN = 'sonos_unjoin'
@ -289,6 +290,7 @@ class SonosDevice(MediaPlayerDevice):
self._media_next_title = None self._media_next_title = None
self._support_previous_track = False self._support_previous_track = False
self._support_next_track = False self._support_next_track = False
self._support_stop = False
self._support_pause = False self._support_pause = False
self._current_track_uri = None self._current_track_uri = None
self._current_track_is_radio_stream = False self._current_track_is_radio_stream = False
@ -433,6 +435,7 @@ class SonosDevice(MediaPlayerDevice):
support_previous_track = False support_previous_track = False
support_next_track = False support_next_track = False
support_stop = False
support_pause = False support_pause = False
if is_playing_tv: if is_playing_tv:
@ -450,6 +453,7 @@ class SonosDevice(MediaPlayerDevice):
) )
support_previous_track = False support_previous_track = False
support_next_track = False support_next_track = False
support_stop = False
support_pause = False support_pause = False
# for radio streams we set the radio station name as the # for radio streams we set the radio station name as the
@ -506,6 +510,7 @@ class SonosDevice(MediaPlayerDevice):
) )
support_previous_track = True support_previous_track = True
support_next_track = True support_next_track = True
support_stop = True
support_pause = True support_pause = True
position_info = self._player.avTransport.GetPositionInfo( position_info = self._player.avTransport.GetPositionInfo(
@ -583,6 +588,7 @@ class SonosDevice(MediaPlayerDevice):
self._current_track_is_radio_stream = is_radio_stream self._current_track_is_radio_stream = is_radio_stream
self._support_previous_track = support_previous_track self._support_previous_track = support_previous_track
self._support_next_track = support_next_track self._support_next_track = support_next_track
self._support_stop = support_stop
self._support_pause = support_pause self._support_pause = support_pause
self._is_playing_tv = is_playing_tv self._is_playing_tv = is_playing_tv
self._is_playing_line_in = is_playing_line_in self._is_playing_line_in = is_playing_line_in
@ -614,6 +620,7 @@ class SonosDevice(MediaPlayerDevice):
self._current_track_is_radio_stream = False self._current_track_is_radio_stream = False
self._support_previous_track = False self._support_previous_track = False
self._support_next_track = False self._support_next_track = False
self._support_stop = False
self._support_pause = False self._support_pause = False
self._is_playing_tv = False self._is_playing_tv = False
self._is_playing_line_in = False self._is_playing_line_in = False
@ -774,6 +781,9 @@ class SonosDevice(MediaPlayerDevice):
if not self._support_next_track: if not self._support_next_track:
supported = supported ^ SUPPORT_NEXT_TRACK supported = supported ^ SUPPORT_NEXT_TRACK
if not self._support_stop:
supported = supported ^ SUPPORT_STOP
if not self._support_pause: if not self._support_pause:
supported = supported ^ SUPPORT_PAUSE supported = supported ^ SUPPORT_PAUSE
@ -836,6 +846,13 @@ class SonosDevice(MediaPlayerDevice):
else: else:
self._player.play() self._player.play()
def media_stop(self):
"""Send stop command."""
if self._coordinator:
self._coordinator.media_stop()
else:
self._player.stop()
def media_pause(self): def media_pause(self):
"""Send pause command.""" """Send pause command."""
if self._coordinator: if self._coordinator:

View File

@ -14,7 +14,7 @@ from homeassistant.components.media_player import (
ATTR_MEDIA_CONTENT_TYPE, ATTR_MEDIA_DURATION, ATTR_MEDIA_EPISODE, ATTR_MEDIA_CONTENT_TYPE, ATTR_MEDIA_DURATION, ATTR_MEDIA_EPISODE,
ATTR_MEDIA_PLAYLIST, ATTR_MEDIA_SEASON, ATTR_MEDIA_SEEK_POSITION, ATTR_MEDIA_PLAYLIST, ATTR_MEDIA_SEASON, ATTR_MEDIA_SEEK_POSITION,
ATTR_MEDIA_SERIES_TITLE, ATTR_MEDIA_TITLE, ATTR_MEDIA_TRACK, ATTR_MEDIA_SERIES_TITLE, ATTR_MEDIA_TITLE, ATTR_MEDIA_TRACK,
ATTR_MEDIA_VOLUME_LEVEL, ATTR_MEDIA_VOLUME_MUTED, ATTR_MEDIA_VOLUME_LEVEL, ATTR_MEDIA_VOLUME_MUTED, ATTR_INPUT_SOURCE_LIST,
ATTR_SUPPORTED_MEDIA_COMMANDS, DOMAIN, SERVICE_PLAY_MEDIA, ATTR_SUPPORTED_MEDIA_COMMANDS, DOMAIN, SERVICE_PLAY_MEDIA,
SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
SUPPORT_VOLUME_STEP, SUPPORT_SELECT_SOURCE, SUPPORT_CLEAR_PLAYLIST, SUPPORT_VOLUME_STEP, SUPPORT_SELECT_SOURCE, SUPPORT_CLEAR_PLAYLIST,
@ -38,6 +38,7 @@ CONF_COMMANDS = 'commands'
CONF_PLATFORM = 'platform' CONF_PLATFORM = 'platform'
CONF_SERVICE = 'service' CONF_SERVICE = 'service'
CONF_SERVICE_DATA = 'service_data' CONF_SERVICE_DATA = 'service_data'
ATTR_DATA = 'data'
CONF_STATE = 'state' CONF_STATE = 'state'
OFF_STATES = [STATE_IDLE, STATE_OFF] OFF_STATES = [STATE_IDLE, STATE_OFF]
@ -178,14 +179,15 @@ class UniversalMediaPlayer(MediaPlayerDevice):
def _call_service(self, service_name, service_data=None, def _call_service(self, service_name, service_data=None,
allow_override=False): allow_override=False):
"""Call either a specified or active child's service.""" """Call either a specified or active child's service."""
if allow_override and service_name in self._cmds:
call_from_config(
self.hass, self._cmds[service_name], blocking=True)
return
if service_data is None: if service_data is None:
service_data = {} service_data = {}
if allow_override and service_name in self._cmds:
call_from_config(
self.hass, self._cmds[service_name],
variables=service_data, blocking=True)
return
active_child = self._child_state active_child = self._child_state
service_data[ATTR_ENTITY_ID] = active_child.entity_id service_data[ATTR_ENTITY_ID] = active_child.entity_id
@ -233,7 +235,7 @@ class UniversalMediaPlayer(MediaPlayerDevice):
@property @property
def volume_level(self): def volume_level(self):
"""Volume level of entity specified in attributes or active child.""" """Volume level of entity specified in attributes or active child."""
return self._child_attr(ATTR_MEDIA_VOLUME_LEVEL) return self._override_or_child_attr(ATTR_MEDIA_VOLUME_LEVEL)
@property @property
def is_volume_muted(self): def is_volume_muted(self):
@ -261,6 +263,17 @@ class UniversalMediaPlayer(MediaPlayerDevice):
"""Image url of current playing media.""" """Image url of current playing media."""
return self._child_attr(ATTR_ENTITY_PICTURE) return self._child_attr(ATTR_ENTITY_PICTURE)
@property
def entity_picture(self):
"""
Return image of the media playing.
The universal media player doesn't use the parent class logic, since
the url is coming from child entity pictures which have already been
sent through the API proxy.
"""
return self.media_image_url
@property @property
def media_title(self): def media_title(self):
"""Title of current playing media.""" """Title of current playing media."""
@ -322,9 +335,14 @@ class UniversalMediaPlayer(MediaPlayerDevice):
return self._child_attr(ATTR_APP_NAME) return self._child_attr(ATTR_APP_NAME)
@property @property
def current_source(self): def source(self):
""""Return the current input source of the device.""" """"Return the current input source of the device."""
return self._child_attr(ATTR_INPUT_SOURCE) return self._override_or_child_attr(ATTR_INPUT_SOURCE)
@property
def source_list(self):
"""List of available input sources."""
return self._override_or_child_attr(ATTR_INPUT_SOURCE_LIST)
@property @property
def supported_media_commands(self): def supported_media_commands(self):
@ -340,6 +358,8 @@ class UniversalMediaPlayer(MediaPlayerDevice):
SERVICE_VOLUME_DOWN]]): SERVICE_VOLUME_DOWN]]):
flags |= SUPPORT_VOLUME_STEP flags |= SUPPORT_VOLUME_STEP
flags &= ~SUPPORT_VOLUME_SET flags &= ~SUPPORT_VOLUME_SET
elif SERVICE_VOLUME_SET in self._cmds:
flags |= SUPPORT_VOLUME_SET
if SERVICE_VOLUME_MUTE in self._cmds and \ if SERVICE_VOLUME_MUTE in self._cmds and \
ATTR_MEDIA_VOLUME_MUTED in self._attrs: ATTR_MEDIA_VOLUME_MUTED in self._attrs:
@ -376,7 +396,7 @@ class UniversalMediaPlayer(MediaPlayerDevice):
def set_volume_level(self, volume_level): def set_volume_level(self, volume_level):
"""Set volume level, range 0..1.""" """Set volume level, range 0..1."""
data = {ATTR_MEDIA_VOLUME_LEVEL: volume_level} data = {ATTR_MEDIA_VOLUME_LEVEL: volume_level}
self._call_service(SERVICE_VOLUME_SET, data) self._call_service(SERVICE_VOLUME_SET, data, allow_override=True)
def media_play(self): def media_play(self):
"""Send play commmand.""" """Send play commmand."""
@ -424,7 +444,7 @@ class UniversalMediaPlayer(MediaPlayerDevice):
def select_source(self, source): def select_source(self, source):
"""Set the input source.""" """Set the input source."""
data = {ATTR_INPUT_SOURCE: source} data = {ATTR_INPUT_SOURCE: source}
self._call_service(SERVICE_SELECT_SOURCE, data) self._call_service(SERVICE_SELECT_SOURCE, data, allow_override=True)
def clear_playlist(self): def clear_playlist(self):
"""Clear players playlist.""" """Clear players playlist."""

View File

@ -0,0 +1,152 @@
"""
Provide functionality to interact with vlc devices on the network.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/media_player.vlc/
"""
import logging
import voluptuous as vol
from homeassistant.components.media_player import (
SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
MediaPlayerDevice, PLATFORM_SCHEMA, MEDIA_TYPE_MUSIC)
from homeassistant.const import (CONF_NAME, STATE_IDLE, STATE_PAUSED,
STATE_PLAYING)
import homeassistant.helpers.config_validation as cv
import homeassistant.util.dt as dt_util
REQUIREMENTS = ['python-vlc==1.1.2']
_LOGGER = logging.getLogger(__name__)
SUPPORT_VLC = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
SUPPORT_PLAY_MEDIA
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_NAME): cv.string,
})
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the vlc platform."""
add_devices([VlcDevice(config.get(CONF_NAME))])
class VlcDevice(MediaPlayerDevice):
"""Representation of a vlc player."""
def __init__(self, name):
"""Initialize the vlc device."""
import vlc
self._instance = vlc.Instance()
self._vlc = self._instance.media_player_new()
self._name = name
self._volume = None
self._muted = None
self._state = None
self._media_position_updated_at = None
self._media_position = None
self._media_duration = None
def update(self):
"""Get the latest details from the device."""
import vlc
status = self._vlc.get_state()
if status == vlc.State.Playing:
self._state = STATE_PLAYING
elif status == vlc.State.Paused:
self._state = STATE_PAUSED
else:
self._state = STATE_IDLE
self._media_duration = self._vlc.get_length()/1000
self._media_position = self._vlc.get_position() * self._media_duration
self._media_position_updated_at = dt_util.utcnow()
self._volume = self._vlc.audio_get_volume() / 100
self._muted = (self._vlc.audio_get_mute() == 1)
return True
@property
def name(self):
"""Return the name of the device."""
return self._name
@property
def state(self):
"""Return the state of the device."""
return self._state
@property
def volume_level(self):
"""Volume level of the media player (0..1)."""
return self._volume
@property
def is_volume_muted(self):
"""Boolean if volume is currently muted."""
return self._muted
@property
def supported_media_commands(self):
"""Flag of media commands that are supported."""
return SUPPORT_VLC
@property
def media_content_type(self):
"""Content type of current playing media."""
return MEDIA_TYPE_MUSIC
@property
def media_duration(self):
"""Duration of current playing media in seconds."""
return self._media_duration
@property
def media_position(self):
"""Position of current playing media in seconds."""
return self._media_position
@property
def media_position_updated_at(self):
"""When was the position of the current playing media valid."""
return self._media_position_updated_at
def media_seek(self, position):
"""Seek the media to a specific location."""
track_length = self._vlc.get_length()/1000
self._vlc.set_position(position/track_length)
def mute_volume(self, mute):
"""Mute the volume."""
self._vlc.audio_set_mute(mute)
self._muted = mute
def set_volume_level(self, volume):
"""Set volume level, range 0..1."""
self._vlc.audio_set_volume(int(volume * 100))
self._volume = volume
def media_play(self):
"""Send play commmand."""
self._vlc.play()
self._state = STATE_PLAYING
def media_pause(self):
"""Send pause command."""
self._vlc.pause()
self._state = STATE_PAUSED
def play_media(self, media_type, media_id, **kwargs):
"""Play media from a URL or file."""
if not media_type == MEDIA_TYPE_MUSIC:
_LOGGER.error(
"Invalid media type %s. Only %s is supported",
media_type, MEDIA_TYPE_MUSIC)
return
self._vlc.set_media(self._instance.media_new(media_id))
self._vlc.play()
self._state = STATE_PLAYING

View File

@ -18,7 +18,7 @@ from homeassistant.util import Throttle
REQUIREMENTS = [ REQUIREMENTS = [
'https://github.com/jabesq/netatmo-api-python/archive/' 'https://github.com/jabesq/netatmo-api-python/archive/'
'v0.7.0.zip#lnetatmo==0.7.0'] 'v0.8.0.zip#lnetatmo==0.8.0']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -30,6 +30,7 @@ NETATMO_AUTH = None
DEFAULT_DISCOVERY = True DEFAULT_DISCOVERY = True
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=10) MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=10)
MIN_TIME_BETWEEN_EVENT_UPDATES = timedelta(seconds=10)
CONFIG_SCHEMA = vol.Schema({ CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({ DOMAIN: vol.Schema({
@ -72,10 +73,11 @@ class WelcomeData(object):
self.auth = auth self.auth = auth
self.welcomedata = None self.welcomedata = None
self.camera_names = [] self.camera_names = []
self.module_names = []
self.home = home self.home = home
def get_camera_names(self): def get_camera_names(self):
"""Return all module available on the API as a list.""" """Return all camera available on the API as a list."""
self.camera_names = [] self.camera_names = []
self.update() self.update()
if not self.home: if not self.home:
@ -87,8 +89,24 @@ class WelcomeData(object):
self.camera_names.append(camera['name']) self.camera_names.append(camera['name'])
return self.camera_names return self.camera_names
def get_module_names(self, camera_name):
"""Return all module available on the API as a list."""
self.module_names = []
self.update()
cam_id = self.welcomedata.cameraByName(camera=camera_name,
home=self.home)['id']
for module in self.welcomedata.modules.values():
if cam_id == module['cam_id']:
self.module_names.append(module['name'])
return self.module_names
@Throttle(MIN_TIME_BETWEEN_UPDATES) @Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self): def update(self):
"""Call the Netatmo API to update the data.""" """Call the Netatmo API to update the data."""
import lnetatmo import lnetatmo
self.welcomedata = lnetatmo.WelcomeData(self.auth) self.welcomedata = lnetatmo.WelcomeData(self.auth, size=100)
@Throttle(MIN_TIME_BETWEEN_EVENT_UPDATES)
def update_event(self):
"""Call the Netatmo API to update the list of events."""
self.welcomedata.updateEvent(home=self.home)

View File

@ -9,6 +9,7 @@ import smtplib
from email.mime.multipart import MIMEMultipart from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText from email.mime.text import MIMEText
from email.mime.image import MIMEImage from email.mime.image import MIMEImage
import email.utils
import voluptuous as vol import voluptuous as vol
@ -18,6 +19,7 @@ from homeassistant.components.notify import (
from homeassistant.const import ( from homeassistant.const import (
CONF_USERNAME, CONF_PASSWORD, CONF_PORT, CONF_SENDER, CONF_RECIPIENT) CONF_USERNAME, CONF_PASSWORD, CONF_PORT, CONF_SENDER, CONF_RECIPIENT)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
import homeassistant.util.dt as dt_util
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -134,6 +136,8 @@ class MailNotificationService(BaseNotificationService):
msg['To'] = self.recipient msg['To'] = self.recipient
msg['From'] = self._sender msg['From'] = self._sender
msg['X-Mailer'] = 'HomeAssistant' msg['X-Mailer'] = 'HomeAssistant'
msg['Date'] = email.utils.format_datetime(dt_util.now())
msg['Message-Id'] = email.utils.make_msgid()
return self._send_email(msg) return self._send_email(msg)

View File

@ -77,7 +77,7 @@ class CoinMarketCapSensor(Entity):
@property @property
def state(self): def state(self):
"""Return the state of the sensor.""" """Return the state of the sensor."""
return self._ticker.get('price_usd') return round(self._ticker.get('price_usd'), 2)
@property @property
def unit_of_measurement(self): def unit_of_measurement(self):

View File

@ -26,15 +26,15 @@ stores/caches the latest telegram and notifies the Entities that the telegram
has been updated. has been updated.
""" """
import asyncio import asyncio
import logging
from datetime import timedelta from datetime import timedelta
import logging
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import CONF_PORT, EVENT_HOMEASSISTANT_STOP from homeassistant.const import (
CONF_PORT, EVENT_HOMEASSISTANT_STOP, STATE_UNKNOWN)
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
import voluptuous as vol
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -65,30 +65,37 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
# Suppress logging # Suppress logging
logging.getLogger('dsmr_parser').setLevel(logging.ERROR) logging.getLogger('dsmr_parser').setLevel(logging.ERROR)
from dsmr_parser import obis_references as obis from dsmr_parser import obis_references as obis_ref
from dsmr_parser.protocol import create_dsmr_reader from dsmr_parser.protocol import create_dsmr_reader
dsmr_version = config[CONF_DSMR_VERSION] dsmr_version = config[CONF_DSMR_VERSION]
# Define list of name,obis mappings to generate entities # Define list of name,obis mappings to generate entities
obis_mapping = [ obis_mapping = [
['Power Consumption', obis.CURRENT_ELECTRICITY_USAGE], ['Power Consumption', obis_ref.CURRENT_ELECTRICITY_USAGE],
['Power Production', obis.CURRENT_ELECTRICITY_DELIVERY], ['Power Production', obis_ref.CURRENT_ELECTRICITY_DELIVERY],
['Power Tariff', obis.ELECTRICITY_ACTIVE_TARIFF], ['Power Tariff', obis_ref.ELECTRICITY_ACTIVE_TARIFF],
['Power Consumption (low)', obis.ELECTRICITY_USED_TARIFF_1], ['Power Consumption (low)', obis_ref.ELECTRICITY_USED_TARIFF_1],
['Power Consumption (normal)', obis.ELECTRICITY_USED_TARIFF_2], ['Power Consumption (normal)', obis_ref.ELECTRICITY_USED_TARIFF_2],
['Power Production (low)', obis.ELECTRICITY_DELIVERED_TARIFF_1], ['Power Production (low)', obis_ref.ELECTRICITY_DELIVERED_TARIFF_1],
['Power Production (normal)', obis.ELECTRICITY_DELIVERED_TARIFF_2], ['Power Production (normal)', obis_ref.ELECTRICITY_DELIVERED_TARIFF_2],
] ]
# Protocol version specific obis
if dsmr_version == '4':
obis_mapping.append(['Gas Consumption', obis.HOURLY_GAS_METER_READING])
else:
obis_mapping.append(['Gas Consumption', obis.GAS_METER_READING])
# Generate device entities # Generate device entities
devices = [DSMREntity(name, obis) for name, obis in obis_mapping] devices = [DSMREntity(name, obis) for name, obis in obis_mapping]
# Protocol version specific obis
if dsmr_version == '4':
gas_obis = obis_ref.HOURLY_GAS_METER_READING
else:
gas_obis = obis_ref.GAS_METER_READING
# add gas meter reading and derivative for usage
devices += [
DSMREntity('Gas Consumption', gas_obis),
DerivativeDSMREntity('Hourly Gas Consumption', gas_obis),
]
yield from async_add_devices(devices) yield from async_add_devices(devices)
def update_entities_telegram(telegram): def update_entities_telegram(telegram):
@ -151,7 +158,10 @@ class DSMREntity(Entity):
if self._obis == obis.ELECTRICITY_ACTIVE_TARIFF: if self._obis == obis.ELECTRICITY_ACTIVE_TARIFF:
return self.translate_tariff(value) return self.translate_tariff(value)
else: else:
return value if value:
return value
else:
return STATE_UNKNOWN
@property @property
def unit_of_measurement(self): def unit_of_measurement(self):
@ -168,4 +178,55 @@ class DSMREntity(Entity):
elif value == '0001': elif value == '0001':
return 'low' return 'low'
else: else:
return None return STATE_UNKNOWN
class DerivativeDSMREntity(DSMREntity):
"""Calculated derivative for values where the DSMR doesn't offer one.
Gas readings are only reported per hour and don't offer a rate only
the current meter reading. This entity converts subsequents readings
into a hourly rate.
"""
_previous_reading = None
_previous_timestamp = None
_state = STATE_UNKNOWN
@property
def state(self):
"""Return the calculated current hourly rate."""
return self._state
@asyncio.coroutine
def async_update(self):
"""Recalculate hourly rate if timestamp has changed.
DSMR updates gas meter reading every hour. Along with the
new value a timestamp is provided for the reading. Test
if the last known timestamp differs from the current one
then calculate a new rate for the previous hour.
"""
# check if the timestamp for the object differs from the previous one
timestamp = self.get_dsmr_object_attr('datetime')
if timestamp and timestamp != self._previous_timestamp:
current_reading = self.get_dsmr_object_attr('value')
if self._previous_reading is None:
# can't calculate rate without previous datapoint
# just store current point
pass
else:
# recalculate the rate
diff = current_reading - self._previous_reading
self._state = diff
self._previous_reading = current_reading
self._previous_timestamp = timestamp
@property
def unit_of_measurement(self):
"""Return the unit of measurement of this entity, per hour, if any."""
unit = self.get_dsmr_object_attr('unit')
if unit:
return unit + '/h'

View File

@ -38,14 +38,19 @@ SENSOR_TYPES = {
'sum_rain_1': ['sum_rain_1', 'mm', 'mdi:weather-rainy'], 'sum_rain_1': ['sum_rain_1', 'mm', 'mdi:weather-rainy'],
'sum_rain_24': ['sum_rain_24', 'mm', 'mdi:weather-rainy'], 'sum_rain_24': ['sum_rain_24', 'mm', 'mdi:weather-rainy'],
'battery_vp': ['Battery', '', 'mdi:battery'], 'battery_vp': ['Battery', '', 'mdi:battery'],
'battery_lvl': ['Battery_lvl', '', 'mdi:battery'],
'min_temp': ['Min Temp.', TEMP_CELSIUS, 'mdi:thermometer'], 'min_temp': ['Min Temp.', TEMP_CELSIUS, 'mdi:thermometer'],
'max_temp': ['Max Temp.', TEMP_CELSIUS, 'mdi:thermometer'], 'max_temp': ['Max Temp.', TEMP_CELSIUS, 'mdi:thermometer'],
'WindAngle': ['Angle', '', 'mdi:compass'], 'WindAngle': ['Angle', '', 'mdi:compass'],
'WindAngle_value': ['Angle Value', 'º', 'mdi:compass'],
'WindStrength': ['Strength', 'km/h', 'mdi:weather-windy'], 'WindStrength': ['Strength', 'km/h', 'mdi:weather-windy'],
'GustAngle': ['Gust Angle', '', 'mdi:compass'], 'GustAngle': ['Gust Angle', '', 'mdi:compass'],
'GustAngle_value': ['Gust Angle Value', 'º', 'mdi:compass'],
'GustStrength': ['Gust Strength', 'km/h', 'mdi:weather-windy'], 'GustStrength': ['Gust Strength', 'km/h', 'mdi:weather-windy'],
'rf_status': ['Radio', '', 'mdi:signal'], 'rf_status': ['Radio', '', 'mdi:signal'],
'wifi_status': ['Wifi', '', 'mdi:wifi'] 'rf_status_lvl': ['Radio_lvl', '', 'mdi:signal'],
'wifi_status': ['Wifi', '', 'mdi:wifi'],
'wifi_status_lvl': ['Wifi_lvl', 'dBm', 'mdi:wifi']
} }
MODULE_SCHEMA = vol.Schema({ MODULE_SCHEMA = vol.Schema({
@ -103,6 +108,7 @@ class NetAtmoSensor(Entity):
self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] self._unit_of_measurement = SENSOR_TYPES[sensor_type][1]
module_id = self.netatmo_data.\ module_id = self.netatmo_data.\
station_data.moduleByName(module=module_name)['_id'] station_data.moduleByName(module=module_name)['_id']
self.module_id = module_id[1]
self._unique_id = "Netatmo Sensor {0} - {1} ({2})".format(self._name, self._unique_id = "Netatmo Sensor {0} - {1} ({2})".format(self._name,
module_id, module_id,
self.type) self.type)
@ -154,21 +160,58 @@ class NetAtmoSensor(Entity):
self._state = data['CO2'] self._state = data['CO2']
elif self.type == 'pressure': elif self.type == 'pressure':
self._state = round(data['Pressure'], 1) self._state = round(data['Pressure'], 1)
elif self.type == 'battery_vp': elif self.type == 'battery_lvl':
self._state = data['battery_vp']
elif self.type == 'battery_vp' and self.module_id == '6':
if data['battery_vp'] >= 5590:
self._state = "Full"
elif data['battery_vp'] >= 5180:
self._state = "High"
elif data['battery_vp'] >= 4770:
self._state = "Medium"
elif data['battery_vp'] >= 4360:
self._state = "Low"
elif data['battery_vp'] < 4360:
self._state = "Very Low"
elif self.type == 'battery_vp' and self.module_id == '5':
if data['battery_vp'] >= 5500: if data['battery_vp'] >= 5500:
self._state = "Full" self._state = "Full"
elif data['battery_vp'] >= 5100: elif data['battery_vp'] >= 5000:
self._state = "High" self._state = "High"
elif data['battery_vp'] >= 4600: elif data['battery_vp'] >= 4500:
self._state = "Medium" self._state = "Medium"
elif data['battery_vp'] >= 4100: elif data['battery_vp'] >= 4000:
self._state = "Low" self._state = "Low"
elif data['battery_vp'] < 4100: elif data['battery_vp'] < 4000:
self._state = "Very Low"
elif self.type == 'battery_vp' and self.module_id == '3':
if data['battery_vp'] >= 5640:
self._state = "Full"
elif data['battery_vp'] >= 5280:
self._state = "High"
elif data['battery_vp'] >= 4920:
self._state = "Medium"
elif data['battery_vp'] >= 4560:
self._state = "Low"
elif data['battery_vp'] < 4560:
self._state = "Very Low"
elif self.type == 'battery_vp' and self.module_id == '2':
if data['battery_vp'] >= 5500:
self._state = "Full"
elif data['battery_vp'] >= 5000:
self._state = "High"
elif data['battery_vp'] >= 4500:
self._state = "Medium"
elif data['battery_vp'] >= 4000:
self._state = "Low"
elif data['battery_vp'] < 4000:
self._state = "Very Low" self._state = "Very Low"
elif self.type == 'min_temp': elif self.type == 'min_temp':
self._state = data['min_temp'] self._state = data['min_temp']
elif self.type == 'max_temp': elif self.type == 'max_temp':
self._state = data['max_temp'] self._state = data['max_temp']
elif self.type == 'WindAngle_value':
self._state = data['WindAngle']
elif self.type == 'WindAngle': elif self.type == 'WindAngle':
if data['WindAngle'] >= 330: if data['WindAngle'] >= 330:
self._state = "North (%d\xb0)" % data['WindAngle'] self._state = "North (%d\xb0)" % data['WindAngle']
@ -190,6 +233,8 @@ class NetAtmoSensor(Entity):
self._state = "North (%d\xb0)" % data['WindAngle'] self._state = "North (%d\xb0)" % data['WindAngle']
elif self.type == 'WindStrength': elif self.type == 'WindStrength':
self._state = data['WindStrength'] self._state = data['WindStrength']
elif self.type == 'GustAngle_value':
self._state = data['GustAngle']
elif self.type == 'GustAngle': elif self.type == 'GustAngle':
if data['GustAngle'] >= 330: if data['GustAngle'] >= 330:
self._state = "North (%d\xb0)" % data['GustAngle'] self._state = "North (%d\xb0)" % data['GustAngle']
@ -211,6 +256,8 @@ class NetAtmoSensor(Entity):
self._state = "North (%d\xb0)" % data['GustAngle'] self._state = "North (%d\xb0)" % data['GustAngle']
elif self.type == 'GustStrength': elif self.type == 'GustStrength':
self._state = data['GustStrength'] self._state = data['GustStrength']
elif self.type == 'rf_status_lvl':
self._state = data['rf_status']
elif self.type == 'rf_status': elif self.type == 'rf_status':
if data['rf_status'] >= 90: if data['rf_status'] >= 90:
self._state = "Low" self._state = "Low"
@ -220,13 +267,17 @@ class NetAtmoSensor(Entity):
self._state = "High" self._state = "High"
elif data['rf_status'] <= 59: elif data['rf_status'] <= 59:
self._state = "Full" self._state = "Full"
elif self.type == 'wifi_status_lvl':
self._state = data['wifi_status']
elif self.type == 'wifi_status': elif self.type == 'wifi_status':
if data['wifi_status'] >= 86: if data['wifi_status'] >= 86:
self._state = "Bad" self._state = "Low"
elif data['wifi_status'] >= 71: elif data['wifi_status'] >= 71:
self._state = "Middle" self._state = "Medium"
elif data['wifi_status'] <= 70: elif data['wifi_status'] >= 56:
self._state = "Good" self._state = "High"
elif data['wifi_status'] <= 55:
self._state = "Full"
class NetAtmoData(object): class NetAtmoData(object):
@ -248,7 +299,7 @@ class NetAtmoData(object):
def update(self): def update(self):
"""Call the Netatmo API to update the data.""" """Call the Netatmo API to update the data."""
import lnetatmo import lnetatmo
self.station_data = lnetatmo.DeviceList(self.auth) self.station_data = lnetatmo.WeatherStationData(self.auth)
if self.station is not None: if self.station is not None:
self.data = self.station_data.lastData(station=self.station, self.data = self.station_data.lastData(station=self.station,

View File

@ -0,0 +1,147 @@
"""
Support gathering system information of hosts which are running netdata.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.netdata/
"""
import logging
from datetime import timedelta
import requests
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import (
CONF_HOST, CONF_PORT, STATE_UNKNOWN, CONF_NAME, CONF_RESOURCES)
from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
_LOGGER = logging.getLogger(__name__)
_RESOURCE = 'api/v1'
_REALTIME = 'before=0&after=-1&options=seconds'
DEFAULT_HOST = 'localhost'
DEFAULT_NAME = 'Netdata'
DEFAULT_PORT = '19999'
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)
SENSOR_TYPES = {
'memory_free': ['RAM Free', 'MiB', 'system.ram', 'free', 1],
'memory_used': ['RAM Used', 'MiB', 'system.ram', 'used', 1],
'memory_cached': ['RAM Cached', 'MiB', 'system.ram', 'cached', 1],
'memory_buffers': ['RAM Buffers', 'MiB', 'system.ram', 'buffers', 1],
'swap_free': ['Swap Free', 'MiB', 'system.swap', 'free', 1],
'swap_used': ['Swap Used', 'MiB', 'system.swap', 'used', 1],
'processes_running': ['Processes Running', 'Count', 'system.processes',
'running', 0],
'processes_blocked': ['Processes Blocked', 'Count', 'system.processes',
'blocked', 0],
'system_load': ['System Load', '15 min', 'system.processes', 'running', 2],
'system_io_in': ['System IO In', 'Count', 'system.io', 'in', 0],
'system_io_out': ['System IO Out', 'Count', 'system.io', 'out', 0],
}
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_RESOURCES, default=['memory_free']):
vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]),
})
# pylint: disable=unused-variable
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Netdata sensor."""
name = config.get(CONF_NAME)
host = config.get(CONF_HOST)
port = config.get(CONF_PORT)
url = 'http://{}:{}'.format(host, port)
version_url = '{}/version.txt'.format(url)
data_url = '{}/{}/data?chart='.format(url, _RESOURCE)
resources = config.get(CONF_RESOURCES)
try:
response = requests.get(version_url, timeout=10)
if not response.ok:
_LOGGER.error("Response status is '%s'", response.status_code)
return False
except requests.exceptions.ConnectionError:
_LOGGER.error("No route to resource/endpoint: %s", url)
return False
values = {}
for key, value in sorted(SENSOR_TYPES.items()):
if key in resources:
values.setdefault(value[2], []).append(key)
dev = []
for chart in values:
rest_url = '{}{}&{}'.format(data_url, chart, _REALTIME)
rest = NetdataData(rest_url)
for sensor_type in values[chart]:
dev.append(NetdataSensor(rest, name, sensor_type))
add_devices(dev)
class NetdataSensor(Entity):
"""Implementation of a Netdata sensor."""
def __init__(self, rest, name, sensor_type):
"""Initialize the sensor."""
self.rest = rest
self.type = sensor_type
self._name = '{} {}'.format(name, SENSOR_TYPES[self.type][0])
self._precision = SENSOR_TYPES[self.type][4]
self._unit_of_measurement = SENSOR_TYPES[self.type][1]
self.update()
@property
def name(self):
"""The name of the sensor."""
return self._name
@property
def unit_of_measurement(self):
"""Return the unit the value is expressed in."""
return self._unit_of_measurement
@property
def state(self):
"""Return the state of the resources."""
value = self.rest.data
if value is not None:
netdata_id = SENSOR_TYPES[self.type][3]
if netdata_id in value:
return "{0:.{1}f}".format(value[netdata_id], self._precision)
else:
return STATE_UNKNOWN
def update(self):
"""Get the latest data from Netdata REST API."""
self.rest.update()
class NetdataData(object):
"""The class for handling the data retrieval."""
def __init__(self, resource):
"""Initialize the data object."""
self._resource = resource
self.data = None
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
"""Get the latest data from the Netdata REST API."""
try:
response = requests.get(self._resource, timeout=5)
det = response.json()
self.data = {k: v for k, v in zip(det['labels'], det['data'][0])}
except requests.exceptions.ConnectionError:
_LOGGER.error("No route to host/endpoint: %s", self._resource)
self.data = None

View File

@ -0,0 +1,134 @@
"""
Support for Sense HAT sensors.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/sensor.sensehat
"""
import os
import logging
from datetime import timedelta
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import (TEMP_CELSIUS, CONF_DISPLAY_OPTIONS, CONF_NAME)
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
REQUIREMENTS = ['sense-hat==2.2.0']
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = 'sensehat'
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)
SENSOR_TYPES = {
'temperature': ['temperature', TEMP_CELSIUS],
'humidity': ['humidity', "%"],
'pressure': ['pressure', "mb"],
}
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_DISPLAY_OPTIONS, default=SENSOR_TYPES):
[vol.In(SENSOR_TYPES)],
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
})
def get_cpu_temp():
"""Get CPU temperature."""
res = os.popen("vcgencmd measure_temp").readline()
t_cpu = float(res.replace("temp=", "").replace("'C\n", ""))
return t_cpu
def get_average(temp_base):
"""Use moving average to get better readings."""
if not hasattr(get_average, "temp"):
get_average.temp = [temp_base, temp_base, temp_base]
get_average.temp[2] = get_average.temp[1]
get_average.temp[1] = get_average.temp[0]
get_average.temp[0] = temp_base
temp_avg = (get_average.temp[0]+get_average.temp[1]+get_average.temp[2])/3
return temp_avg
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the sensor platform."""
data = SenseHatData()
dev = []
for variable in config[CONF_DISPLAY_OPTIONS]:
dev.append(SenseHatSensor(data, variable))
add_devices(dev)
class SenseHatSensor(Entity):
"""Representation of a sensehat sensor."""
def __init__(self, data, sensor_types):
"""Initialize the sensor."""
self.data = data
self._name = SENSOR_TYPES[sensor_types][0]
self._unit_of_measurement = SENSOR_TYPES[sensor_types][1]
self.type = sensor_types
self._state = None
"""updating data."""
self.update()
@property
def name(self):
"""Return the name of the sensor."""
return self._name
@property
def state(self):
"""Return the state of the sensor."""
return self._state
@property
def unit_of_measurement(self):
"""Return the unit the value is expressed in."""
return self._unit_of_measurement
def update(self):
"""Get the latest data and updates the states."""
self.data.update()
if not self.data.humidity:
_LOGGER.error("Don't receive data!")
return
if self.type == 'temperature':
self._state = self.data.temperature
if self.type == 'humidity':
self._state = self.data.humidity
if self.type == 'pressure':
self._state = self.data.pressure
class SenseHatData(object):
"""Get the latest data and update."""
def __init__(self):
"""Initialize the data object."""
self.temperature = None
self.humidity = None
self.pressure = None
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
"""Get the latest data from sensehat."""
from sense_hat import SenseHat
sense = SenseHat()
temp_from_h = sense.get_temperature_from_humidity()
temp_from_p = sense.get_temperature_from_pressure()
t_cpu = get_cpu_temp()
t_total = (temp_from_h+temp_from_p)/2
t_correct = t_total - ((t_cpu-t_total)/1.5)
t_correct = get_average(t_correct)
self.temperature = t_correct
self.humidity = sense.get_humidity()
self.pressure = sense.get_pressure()

View File

@ -6,37 +6,32 @@ https://home-assistant.io/components/sensor.tellduslive/
""" """
import logging import logging
from datetime import datetime
from homeassistant.components import tellduslive from homeassistant.components.tellduslive import TelldusLiveEntity
from homeassistant.const import ( from homeassistant.const import TEMP_CELSIUS
ATTR_BATTERY_LEVEL, DEVICE_DEFAULT_NAME, TEMP_CELSIUS)
from homeassistant.helpers.entity import Entity
ATTR_LAST_UPDATED = "time_last_updated"
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
SENSOR_TYPE_TEMP = "temp" SENSOR_TYPE_TEMP = 'temp'
SENSOR_TYPE_HUMIDITY = "humidity" SENSOR_TYPE_HUMIDITY = 'humidity'
SENSOR_TYPE_RAINRATE = "rrate" SENSOR_TYPE_RAINRATE = 'rrate'
SENSOR_TYPE_RAINTOTAL = "rtot" SENSOR_TYPE_RAINTOTAL = 'rtot'
SENSOR_TYPE_WINDDIRECTION = "wdir" SENSOR_TYPE_WINDDIRECTION = 'wdir'
SENSOR_TYPE_WINDAVERAGE = "wavg" SENSOR_TYPE_WINDAVERAGE = 'wavg'
SENSOR_TYPE_WINDGUST = "wgust" SENSOR_TYPE_WINDGUST = 'wgust'
SENSOR_TYPE_WATT = "watt" SENSOR_TYPE_WATT = 'watt'
SENSOR_TYPE_LUMINANCE = "lum" SENSOR_TYPE_LUMINANCE = 'lum'
SENSOR_TYPES = { SENSOR_TYPES = {
SENSOR_TYPE_TEMP: ['Temperature', TEMP_CELSIUS, "mdi:thermometer"], SENSOR_TYPE_TEMP: ['Temperature', TEMP_CELSIUS, 'mdi:thermometer'],
SENSOR_TYPE_HUMIDITY: ['Humidity', '%', "mdi:water"], SENSOR_TYPE_HUMIDITY: ['Humidity', '%', 'mdi:water'],
SENSOR_TYPE_RAINRATE: ['Rain rate', 'mm', "mdi:water"], SENSOR_TYPE_RAINRATE: ['Rain rate', 'mm', 'mdi:water'],
SENSOR_TYPE_RAINTOTAL: ['Rain total', 'mm', "mdi:water"], SENSOR_TYPE_RAINTOTAL: ['Rain total', 'mm', 'mdi:water'],
SENSOR_TYPE_WINDDIRECTION: ['Wind direction', '', ""], SENSOR_TYPE_WINDDIRECTION: ['Wind direction', '', ''],
SENSOR_TYPE_WINDAVERAGE: ['Wind average', 'm/s', ""], SENSOR_TYPE_WINDAVERAGE: ['Wind average', 'm/s', ''],
SENSOR_TYPE_WINDGUST: ['Wind gust', 'm/s', ""], SENSOR_TYPE_WINDGUST: ['Wind gust', 'm/s', ''],
SENSOR_TYPE_WATT: ['Watt', 'W', ""], SENSOR_TYPE_WATT: ['Watt', 'W', ''],
SENSOR_TYPE_LUMINANCE: ['Luminance', 'lx', ""], SENSOR_TYPE_LUMINANCE: ['Luminance', 'lx', ''],
} }
@ -44,114 +39,75 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup Tellstick sensors.""" """Setup Tellstick sensors."""
if discovery_info is None: if discovery_info is None:
return return
add_devices(TelldusLiveSensor(sensor) for sensor in discovery_info) add_devices(TelldusLiveSensor(hass, sensor) for sensor in discovery_info)
class TelldusLiveSensor(Entity): class TelldusLiveSensor(TelldusLiveEntity):
"""Representation of a Telldus Live sensor.""" """Representation of a Telldus Live sensor."""
def __init__(self, sensor_id): @property
"""Initialize the sensor.""" def device_id(self):
self._id = sensor_id """Return id of the device."""
self.update() return self._id[0]
_LOGGER.debug("created sensor %s", self)
def update(self):
"""Update sensor values."""
tellduslive.NETWORK.update_sensors()
self._sensor = tellduslive.NETWORK.get_sensor(self._id)
@property @property
def _sensor_name(self): def _type(self):
"""Return the name of the sensor."""
return self._sensor["name"]
@property
def _sensor_value(self):
"""Return the value the sensor."""
return self._sensor["data"]["value"]
@property
def _sensor_type(self):
"""Return the type of the sensor.""" """Return the type of the sensor."""
return self._sensor["data"]["name"] return self._id[1]
@property @property
def _battery_level(self): def _value(self):
"""Return the battery level of a sensor.""" """Return value of the sensor."""
sensor_battery_level = self._sensor.get("battery") return self.device.value(self._id[1:])
return round(sensor_battery_level * 100 / 255) \
if sensor_battery_level else None
@property
def _last_updated(self):
"""Return the last update."""
sensor_last_updated = self._sensor.get("lastUpdated")
return str(datetime.fromtimestamp(sensor_last_updated)) \
if sensor_last_updated else None
@property @property
def _value_as_temperature(self): def _value_as_temperature(self):
"""Return the value as temperature.""" """Return the value as temperature."""
return round(float(self._sensor_value), 1) return round(float(self._value), 1)
@property @property
def _value_as_luminance(self): def _value_as_luminance(self):
"""Return the value as luminance.""" """Return the value as luminance."""
return round(float(self._sensor_value), 1) return round(float(self._value), 1)
@property @property
def _value_as_humidity(self): def _value_as_humidity(self):
"""Return the value as humidity.""" """Return the value as humidity."""
return int(round(float(self._sensor_value))) return int(round(float(self._value)))
@property @property
def name(self): def name(self):
"""Return the name of the sensor.""" """Return the name of the sensor."""
return "{} {}".format(self._sensor_name or DEVICE_DEFAULT_NAME, return '{} {}'.format(
self.quantity_name or "") super().name,
self.quantity_name or '')
@property
def available(self):
"""Return true if the sensor is available."""
return not self._sensor.get("offline", False)
@property @property
def state(self): def state(self):
"""Return the state of the sensor.""" """Return the state of the sensor."""
if self._sensor_type == SENSOR_TYPE_TEMP: if self._type == SENSOR_TYPE_TEMP:
return self._value_as_temperature return self._value_as_temperature
elif self._sensor_type == SENSOR_TYPE_HUMIDITY: elif self._type == SENSOR_TYPE_HUMIDITY:
return self._value_as_humidity return self._value_as_humidity
elif self._sensor_type == SENSOR_TYPE_LUMINANCE: elif self._type == SENSOR_TYPE_LUMINANCE:
return self._value_as_luminance return self._value_as_luminance
else: else:
return self._sensor_value return self._value
@property
def device_state_attributes(self):
"""Return the state attributes."""
attrs = {}
if self._battery_level is not None:
attrs[ATTR_BATTERY_LEVEL] = self._battery_level
if self._last_updated is not None:
attrs[ATTR_LAST_UPDATED] = self._last_updated
return attrs
@property @property
def quantity_name(self): def quantity_name(self):
"""Name of quantity.""" """Name of quantity."""
return SENSOR_TYPES[self._sensor_type][0] \ return SENSOR_TYPES[self._type][0] \
if self._sensor_type in SENSOR_TYPES else None if self._type in SENSOR_TYPES else None
@property @property
def unit_of_measurement(self): def unit_of_measurement(self):
"""Return the unit of measurement.""" """Return the unit of measurement."""
return SENSOR_TYPES[self._sensor_type][1] \ return SENSOR_TYPES[self._type][1] \
if self._sensor_type in SENSOR_TYPES else None if self._type in SENSOR_TYPES else None
@property @property
def icon(self): def icon(self):
"""Return the icon.""" """Return the icon."""
return SENSOR_TYPES[self._sensor_type][2] \ return SENSOR_TYPES[self._type][2] \
if self._sensor_type in SENSOR_TYPES else None if self._type in SENSOR_TYPES else None

View File

@ -19,12 +19,15 @@ from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle from homeassistant.util import Throttle
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
_RESOURCE = 'http://api.wunderground.com/api/{}/conditions/q/' _RESOURCE = 'http://api.wunderground.com/api/{}/conditions/{}/q/'
_ALERTS = 'http://api.wunderground.com/api/{}/alerts/q/' _ALERTS = 'http://api.wunderground.com/api/{}/alerts/{}/q/'
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONF_ATTRIBUTION = "Data provided by the WUnderground weather service" CONF_ATTRIBUTION = "Data provided by the WUnderground weather service"
CONF_PWS_ID = 'pws_id' CONF_PWS_ID = 'pws_id'
CONF_LANG = 'lang'
DEFAULT_LANG = 'EN'
MIN_TIME_BETWEEN_UPDATES_ALERTS = timedelta(minutes=15) MIN_TIME_BETWEEN_UPDATES_ALERTS = timedelta(minutes=15)
MIN_TIME_BETWEEN_UPDATES_OBSERVATION = timedelta(minutes=5) MIN_TIME_BETWEEN_UPDATES_OBSERVATION = timedelta(minutes=5)
@ -80,9 +83,29 @@ ALERTS_ATTRS = [
'message', 'message',
] ]
# Language Supported Codes
LANG_CODES = [
'AF', 'AL', 'AR', 'HY', 'AZ', 'EU',
'BY', 'BU', 'LI', 'MY', 'CA', 'CN',
'TW', 'CR', 'CZ', 'DK', 'DV', 'NL',
'EN', 'EO', 'ET', 'FA', 'FI', 'FR',
'FC', 'GZ', 'DL', 'KA', 'GR', 'GU',
'HT', 'IL', 'HI', 'HU', 'IS', 'IO',
'ID', 'IR', 'IT', 'JP', 'JW', 'KM',
'KR', 'KU', 'LA', 'LV', 'LT', 'ND',
'MK', 'MT', 'GM', 'MI', 'MR', 'MN',
'NO', 'OC', 'PS', 'GN', 'PL', 'BR',
'PA', 'PU', 'RO', 'RU', 'SR', 'SK',
'SL', 'SP', 'SI', 'SW', 'CH', 'TL',
'TT', 'TH', 'UA', 'UZ', 'VU', 'CY',
'SN', 'JI', 'YI',
]
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_API_KEY): cv.string, vol.Required(CONF_API_KEY): cv.string,
vol.Optional(CONF_PWS_ID): cv.string, vol.Optional(CONF_PWS_ID): cv.string,
vol.Optional(CONF_LANG, default=DEFAULT_LANG):
vol.All(vol.In(LANG_CODES)),
vol.Required(CONF_MONITORED_CONDITIONS, default=[]): vol.Required(CONF_MONITORED_CONDITIONS, default=[]):
vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]),
}) })
@ -92,7 +115,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the WUnderground sensor.""" """Setup the WUnderground sensor."""
rest = WUndergroundData(hass, rest = WUndergroundData(hass,
config.get(CONF_API_KEY), config.get(CONF_API_KEY),
config.get(CONF_PWS_ID, None)) config.get(CONF_PWS_ID),
config.get(CONF_LANG))
sensors = [] sensors = []
for variable in config[CONF_MONITORED_CONDITIONS]: for variable in config[CONF_MONITORED_CONDITIONS]:
sensors.append(WUndergroundSensor(rest, variable)) sensors.append(WUndergroundSensor(rest, variable))
@ -192,18 +216,19 @@ class WUndergroundSensor(Entity):
class WUndergroundData(object): class WUndergroundData(object):
"""Get data from WUnderground.""" """Get data from WUnderground."""
def __init__(self, hass, api_key, pws_id=None): def __init__(self, hass, api_key, pws_id, lang):
"""Initialize the data object.""" """Initialize the data object."""
self._hass = hass self._hass = hass
self._api_key = api_key self._api_key = api_key
self._pws_id = pws_id self._pws_id = pws_id
self._lang = 'lang:{}'.format(lang)
self._latitude = hass.config.latitude self._latitude = hass.config.latitude
self._longitude = hass.config.longitude self._longitude = hass.config.longitude
self.data = None self.data = None
self.alerts = None self.alerts = None
def _build_url(self, baseurl=_RESOURCE): def _build_url(self, baseurl=_RESOURCE):
url = baseurl.format(self._api_key) url = baseurl.format(self._api_key, self._lang)
if self._pws_id: if self._pws_id:
url = url + 'pws:{}'.format(self._pws_id) url = url + 'pws:{}'.format(self._pws_id)
else: else:

View File

@ -0,0 +1,178 @@
"""
Sensor for data from Austrian "Zentralanstalt für Meteorologie und Geodynamik".
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.zamg/
"""
import csv
import logging
from datetime import timedelta
import requests
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.weather import (
ATTR_WEATHER_HUMIDITY, ATTR_WEATHER_ATTRIBUTION, ATTR_WEATHER_PRESSURE,
ATTR_WEATHER_TEMPERATURE, ATTR_WEATHER_WIND_BEARING,
ATTR_WEATHER_WIND_SPEED)
from homeassistant.const import (
CONF_MONITORED_CONDITIONS, CONF_NAME, __version__)
from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
ATTR_STATION = 'station'
ATTR_UPDATED = 'updated'
ATTRIBUTION = 'Data provided by ZAMG'
CONF_STATION_ID = 'station_id'
DEFAULT_NAME = 'zamg'
# Data source only updates once per hour, so throttle to 30 min to have
# reasonably recent data
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=30)
VALID_STATION_IDS = (
'11010', '11012', '11022', '11035', '11036', '11101', '11121', '11126',
'11130', '11150', '11155', '11157', '11171', '11190', '11204'
)
SENSOR_TYPES = {
ATTR_WEATHER_PRESSURE: ('Pressure', 'hPa', 'LDstat hPa', float),
'pressure_sealevel': ('Pressure at Sea Level', 'hPa', 'LDred hPa', float),
ATTR_WEATHER_HUMIDITY: ('Humidity', '%', 'RF %', int),
ATTR_WEATHER_WIND_SPEED: ('Wind Speed', 'km/h', 'WG km/h', float),
ATTR_WEATHER_WIND_BEARING: ('Wind Bearing', '°', 'WR °', int),
'wind_max_speed': ('Top Wind Speed', 'km/h', 'WSG km/h', float),
'wind_max_bearing': ('Top Wind Bearing', '°', 'WSR °', int),
'sun_last_hour': ('Sun Last Hour', '%', 'SO %', int),
ATTR_WEATHER_TEMPERATURE: ('Temperature', '°C', 'T °C', float),
'precipitation': ('Precipitation', 'l/m²', 'N l/m²', float),
'dewpoint': ('Dew Point', '°C', 'TP °C', float),
# The following probably not useful for general consumption,
# but we need them to fill in internal attributes
'station_name': ('Station Name', None, 'Name', str),
'station_elevation': ('Station Elevation', 'm', 'Höhe m', int),
'update_date': ('Update Date', None, 'Datum', str),
'update_time': ('Update Time', None, 'Zeit', str),
}
PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend({
vol.Required(CONF_MONITORED_CONDITIONS):
vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]),
vol.Required(CONF_STATION_ID):
vol.All(cv.string, vol.In(VALID_STATION_IDS)),
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the ZAMG sensor platform."""
station_id = config.get(CONF_STATION_ID)
name = config.get(CONF_NAME)
logger = logging.getLogger(__name__)
probe = ZamgData(station_id=station_id, logger=logger)
sensors = [ZamgSensor(probe, variable, name)
for variable in config[CONF_MONITORED_CONDITIONS]]
add_devices(sensors, True)
class ZamgSensor(Entity):
"""Implementation of a ZAMG sensor."""
def __init__(self, probe, variable, name):
"""Initialize the sensor."""
self.probe = probe
self.client_name = name
self.variable = variable
self.update()
def update(self):
"""Delegate update to probe."""
self.probe.update()
@property
def name(self):
"""Return the name of the sensor."""
return '{} {}'.format(self.client_name, self.variable)
@property
def state(self):
"""Return the state of the sensor."""
return self.probe.get_data(self.variable)
@property
def unit_of_measurement(self):
"""Return the unit of measurement of this entity, if any."""
return SENSOR_TYPES[self.variable][1]
@property
def state_attributes(self):
"""Return the state attributes."""
return {
ATTR_WEATHER_ATTRIBUTION: ATTRIBUTION,
ATTR_STATION: self.probe.get_data('station_name'),
ATTR_UPDATED: '{} {}'.format(self.probe.get_data('update_date'),
self.probe.get_data('update_time')),
}
class ZamgData(object):
"""The class for handling the data retrieval."""
API_URL = 'http://www.zamg.ac.at/ogd/'
API_FIELDS = {
v[2]: (k, v[3])
for k, v in SENSOR_TYPES.items()
}
API_HEADERS = {
'User-Agent': '{} {}'.format('home-assistant.zamg/', __version__),
}
def __init__(self, logger, station_id):
"""Initialize the probe."""
self._logger = logger
self._station_id = station_id
self.data = {}
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
"""Get the latest data from ZAMG."""
try:
response = requests.get(
self.API_URL, headers=self.API_HEADERS, timeout=15)
except requests.exceptions.RequestException:
self._logger.exception("While fetching data from server")
return
if response.status_code != 200:
self._logger.error("API call returned with status %s",
response.status_code)
return
content_type = response.headers.get('Content-Type', 'whatever')
if content_type != 'text/csv':
self._logger.error("Expected text/csv but got %s", content_type)
return
response.encoding = 'UTF8'
content = response.text
data = (line for line in content.split('\n'))
reader = csv.DictReader(data, delimiter=';', quotechar='"')
for row in reader:
if row.get("Station", None) == self._station_id:
self.data = {
self.API_FIELDS.get(k)[0]:
self.API_FIELDS.get(k)[1](v.replace(',', '.'))
for k, v in row.items()
if v and k in self.API_FIELDS
}
break
def get_data(self, variable):
"""Generic accessor for data."""
return self.data.get(variable)

View File

@ -144,3 +144,12 @@ openalpr:
restart: restart:
description: Restart ffmpeg process of device. description: Restart ffmpeg process of device.
verisure:
capture_smartcam:
description: Capture a new image from a smartcam.
fields:
device_serial:
description: The serial number of the smartcam you want to capture an image from.
example: '2DEU AT5Z'

View File

@ -0,0 +1,148 @@
"""
Support for Digital Loggers DIN III Relays.
Support for Digital Loggers DIN III Relays and possibly other items
through Dwight Hubbard's, python-dlipower.
For more details about python-dlipower, please see
https://github.com/dwighthubbard/python-dlipower
Custom ports are NOT supported due to a limitation of the dlipower
library, not the digital loggers switch
"""
import logging
from datetime import timedelta
import voluptuous as vol
from homeassistant.components.switch import (SwitchDevice, PLATFORM_SCHEMA)
from homeassistant.const import (
CONF_HOST, CONF_NAME, CONF_USERNAME, CONF_PASSWORD, CONF_TIMEOUT)
import homeassistant.helpers.config_validation as cv
from homeassistant.util import Throttle
REQUIREMENTS = ['dlipower==0.7.165']
CONF_CYCLETIME = 'cycletime'
DEFAULT_NAME = 'DINRelay'
DEFAULT_USERNAME = 'admin'
DEFAULT_PASSWORD = 'admin'
DEFAULT_TIMEOUT = 20
DEFAULT_CYCLETIME = 2
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=5)
_LOGGER = logging.getLogger(__name__)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string,
vol.Optional(CONF_PASSWORD, default=DEFAULT_PASSWORD): cv.string,
vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT):
vol.All(vol.Coerce(int), vol.Range(min=1, max=600)),
vol.Optional(CONF_CYCLETIME, default=DEFAULT_CYCLETIME):
vol.All(vol.Coerce(int), vol.Range(min=1, max=600)),
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Find and return DIN III Relay switch."""
import dlipower
host = config.get(CONF_HOST)
controllername = config.get(CONF_NAME)
user = config.get(CONF_USERNAME)
pswd = config.get(CONF_PASSWORD)
tout = config.get(CONF_TIMEOUT)
cycl = config.get(CONF_CYCLETIME)
power_switch = dlipower.PowerSwitch(
hostname=host, userid=user, password=pswd,
timeout=tout, cycletime=cycl
)
if not power_switch.verify():
_LOGGER.error('Could not connect to DIN III Relay')
return False
devices = []
parent_device = DINRelayDevice(power_switch)
devices.extend(
DINRelay(controllername, device.outlet_number, parent_device)
for device in power_switch
)
add_devices(devices)
class DINRelay(SwitchDevice):
"""Representation of a individual DIN III relay port."""
def __init__(self, name, outletnumber, parent_device):
"""Initialize the DIN III Relay switch."""
self._parent_device = parent_device
self.controllername = name
self.outletnumber = outletnumber
self.update()
@property
def name(self):
"""Return the display name of this relay."""
return self._outletname
@property
def is_on(self):
"""Return true if relay is on."""
return self._is_on
@property
def should_poll(self):
"""Polling is needed."""
return True
def turn_on(self, **kwargs):
"""Instruct the relay to turn on."""
self._parent_device.turn_on(outlet=self.outletnumber)
def turn_off(self, **kwargs):
"""Instruct the relay to turn off."""
self._parent_device.turn_off(outlet=self.outletnumber)
def update(self):
"""Trigger update for all switches on the parent device."""
self._parent_device.update()
self._is_on = (
self._parent_device.statuslocal[self.outletnumber - 1][2] == 'ON'
)
self._outletname = "{}_{}".format(
self.controllername,
self._parent_device.statuslocal[self.outletnumber - 1][1]
)
class DINRelayDevice(object):
"""Device representation for per device throttling."""
def __init__(self, device):
"""Initialize the DINRelay device."""
self._device = device
self.update()
def turn_on(self, **kwargs):
"""Instruct the relay to turn on."""
self._device.on(**kwargs)
def turn_off(self, **kwargs):
"""Instruct the relay to turn off."""
self._device.off(**kwargs)
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
"""Fetch new state data for this device."""
self.statuslocal = self._device.statuslist()

View File

@ -11,7 +11,8 @@ import voluptuous as vol
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
import homeassistant.components.pilight as pilight import homeassistant.components.pilight as pilight
from homeassistant.components.switch import (SwitchDevice, PLATFORM_SCHEMA) from homeassistant.components.switch import (SwitchDevice, PLATFORM_SCHEMA)
from homeassistant.const import (CONF_NAME, CONF_ID, CONF_SWITCHES, CONF_STATE) from homeassistant.const import (CONF_NAME, CONF_ID, CONF_SWITCHES, CONF_STATE,
CONF_PROTOCOL)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -21,17 +22,20 @@ CONF_ON_CODE = 'on_code'
CONF_ON_CODE_RECIEVE = 'on_code_receive' CONF_ON_CODE_RECIEVE = 'on_code_receive'
CONF_SYSTEMCODE = 'systemcode' CONF_SYSTEMCODE = 'systemcode'
CONF_UNIT = 'unit' CONF_UNIT = 'unit'
CONF_UNITCODE = 'unitcode'
DEPENDENCIES = ['pilight'] DEPENDENCIES = ['pilight']
COMMAND_SCHEMA = pilight.RF_CODE_SCHEMA.extend({ COMMAND_SCHEMA = vol.Schema({
vol.Optional(CONF_PROTOCOL): cv.string,
vol.Optional('on'): cv.positive_int, vol.Optional('on'): cv.positive_int,
vol.Optional('off'): cv.positive_int, vol.Optional('off'): cv.positive_int,
vol.Optional(CONF_UNIT): cv.positive_int, vol.Optional(CONF_UNIT): cv.positive_int,
vol.Optional(CONF_UNITCODE): cv.positive_int,
vol.Optional(CONF_ID): cv.positive_int, vol.Optional(CONF_ID): cv.positive_int,
vol.Optional(CONF_STATE): cv.string, vol.Optional(CONF_STATE): cv.string,
vol.Optional(CONF_SYSTEMCODE): cv.positive_int, vol.Optional(CONF_SYSTEMCODE): cv.positive_int,
}) }, extra=vol.ALLOW_EXTRA)
SWITCHES_SCHEMA = vol.Schema({ SWITCHES_SCHEMA = vol.Schema({
vol.Required(CONF_ON_CODE): COMMAND_SCHEMA, vol.Required(CONF_ON_CODE): COMMAND_SCHEMA,

View File

@ -9,7 +9,7 @@ https://home-assistant.io/components/switch.tellduslive/
""" """
import logging import logging
from homeassistant.components import tellduslive from homeassistant.components.tellduslive import TelldusLiveEntity
from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity import ToggleEntity
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -19,53 +19,23 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup Tellstick switches.""" """Setup Tellstick switches."""
if discovery_info is None: if discovery_info is None:
return return
add_devices(TelldusLiveSwitch(switch) for switch in discovery_info) add_devices(TelldusLiveSwitch(hass, switch) for switch in discovery_info)
class TelldusLiveSwitch(ToggleEntity): class TelldusLiveSwitch(TelldusLiveEntity, ToggleEntity):
"""Representation of a Tellstick switch.""" """Representation of a Tellstick switch."""
def __init__(self, switch_id):
"""Initialize the switch."""
self._id = switch_id
self.update()
_LOGGER.debug("created switch %s", self)
def update(self):
"""Get the latest date and update the state."""
tellduslive.NETWORK.update_switches()
self._switch = tellduslive.NETWORK.get_switch(self._id)
@property
def should_poll(self):
"""Polling is needed."""
return True
@property
def assumed_state(self):
"""Return true if unable to access real state of entity."""
return True
@property
def name(self):
"""Return the name of the switch if any."""
return self._switch["name"]
@property
def available(self):
"""Return the state of the switch."""
return not self._switch.get("offline", False)
@property @property
def is_on(self): def is_on(self):
"""Return true if switch is on.""" """Return true if switch is on."""
from tellive.live import const return self.device.is_on()
return self._switch["state"] == const.TELLSTICK_TURNON
def turn_on(self, **kwargs): def turn_on(self, **kwargs):
"""Turn the switch on.""" """Turn the switch on."""
tellduslive.NETWORK.turn_switch_on(self._id) self.device.turn_on()
self.changed()
def turn_off(self, **kwargs): def turn_off(self, **kwargs):
"""Turn the switch off.""" """Turn the switch off."""
tellduslive.NETWORK.turn_switch_off(self._id) self.device.turn_off()
self.changed()

View File

@ -26,7 +26,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
signal_repetitions = discovery_info.get(ATTR_DISCOVER_CONFIG, signal_repetitions = discovery_info.get(ATTR_DISCOVER_CONFIG,
DEFAULT_SIGNAL_REPETITIONS) DEFAULT_SIGNAL_REPETITIONS)
add_devices(TellstickSwitch(tellcore_id, signal_repetitions) add_devices(TellstickSwitch(tellcore_id, hass.data['tellcore_registry'],
signal_repetitions)
for tellcore_id in discovery_info[ATTR_DISCOVER_DEVICES]) for tellcore_id in discovery_info[ATTR_DISCOVER_DEVICES])

View File

@ -4,18 +4,20 @@ Support for Telldus Live.
For more details about this component, please refer to the documentation at For more details about this component, please refer to the documentation at
https://home-assistant.io/components/tellduslive/ https://home-assistant.io/components/tellduslive/
""" """
from datetime import datetime, timedelta
import logging import logging
from datetime import timedelta
import voluptuous as vol
from homeassistant.const import ATTR_BATTERY_LEVEL, DEVICE_DEFAULT_NAME
from homeassistant.helpers import discovery from homeassistant.helpers import discovery
from homeassistant.util import Throttle
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import track_point_in_utc_time
from homeassistant.util.dt import utcnow
import voluptuous as vol
DOMAIN = 'tellduslive' DOMAIN = 'tellduslive'
REQUIREMENTS = ['tellive-py==0.5.2'] REQUIREMENTS = ['tellduslive==0.1.9']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -23,11 +25,10 @@ CONF_PUBLIC_KEY = 'public_key'
CONF_PRIVATE_KEY = 'private_key' CONF_PRIVATE_KEY = 'private_key'
CONF_TOKEN = 'token' CONF_TOKEN = 'token'
CONF_TOKEN_SECRET = 'token_secret' CONF_TOKEN_SECRET = 'token_secret'
CONF_UPDATE_INTERVAL = 'update_interval'
MIN_TIME_BETWEEN_SWITCH_UPDATES = timedelta(minutes=1) MIN_UPDATE_INTERVAL = timedelta(seconds=5)
MIN_TIME_BETWEEN_SENSOR_UPDATES = timedelta(minutes=5) DEFAULT_UPDATE_INTERVAL = timedelta(minutes=1)
NETWORK = None
CONFIG_SCHEMA = vol.Schema({ CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({ DOMAIN: vol.Schema({
@ -35,183 +36,190 @@ CONFIG_SCHEMA = vol.Schema({
vol.Required(CONF_PRIVATE_KEY): cv.string, vol.Required(CONF_PRIVATE_KEY): cv.string,
vol.Required(CONF_TOKEN): cv.string, vol.Required(CONF_TOKEN): cv.string,
vol.Required(CONF_TOKEN_SECRET): cv.string, vol.Required(CONF_TOKEN_SECRET): cv.string,
vol.Optional(CONF_UPDATE_INTERVAL, default=DEFAULT_UPDATE_INTERVAL): (
vol.All(cv.time_period, vol.Clamp(min=MIN_UPDATE_INTERVAL)))
}), }),
}, extra=vol.ALLOW_EXTRA) }, extra=vol.ALLOW_EXTRA)
ATTR_LAST_UPDATED = 'time_last_updated'
def setup(hass, config): def setup(hass, config):
"""Setup the Telldus Live component.""" """Setup the Telldus Live component."""
# fixme: aquire app key and provide authentication using username+password client = TelldusLiveClient(hass, config)
global NETWORK if not client.validate_session():
NETWORK = TelldusLiveData(hass, config)
if not NETWORK.validate_session():
_LOGGER.error( _LOGGER.error(
"Authentication Error: " 'Authentication Error: '
"Please make sure you have configured your keys " 'Please make sure you have configured your keys '
"that can be aquired from https://api.telldus.com/keys/index") 'that can be aquired from https://api.telldus.com/keys/index')
return False return False
NETWORK.discover() hass.data[DOMAIN] = client
client.update(utcnow())
return True return True
@Throttle(MIN_TIME_BETWEEN_SWITCH_UPDATES) class TelldusLiveClient(object):
def request_switches():
"""Make request to online service."""
_LOGGER.debug("Updating switches from Telldus Live")
switches = NETWORK.request('devices/list')
# Filter out any group of switches.
if switches and 'device' in switches:
return {switch["id"]: switch for switch in switches['device']
if switch["type"] == "device"}
return None
@Throttle(MIN_TIME_BETWEEN_SENSOR_UPDATES)
def request_sensors():
"""Make request to online service."""
_LOGGER.debug("Updating sensors from Telldus Live")
units = NETWORK.request('sensors/list')
# One unit can contain many sensors.
if units and 'sensor' in units:
return {(unit['id'], sensor['name'], sensor['scale']):
dict(unit, data=sensor)
for unit in units['sensor']
for sensor in unit['data']}
return None
class TelldusLiveData(object):
"""Get the latest data and update the states.""" """Get the latest data and update the states."""
def __init__(self, hass, config): def __init__(self, hass, config):
"""Initialize the Tellus data object.""" """Initialize the Tellus data object."""
from tellduslive import Client
public_key = config[DOMAIN].get(CONF_PUBLIC_KEY) public_key = config[DOMAIN].get(CONF_PUBLIC_KEY)
private_key = config[DOMAIN].get(CONF_PRIVATE_KEY) private_key = config[DOMAIN].get(CONF_PRIVATE_KEY)
token = config[DOMAIN].get(CONF_TOKEN) token = config[DOMAIN].get(CONF_TOKEN)
token_secret = config[DOMAIN].get(CONF_TOKEN_SECRET) token_secret = config[DOMAIN].get(CONF_TOKEN_SECRET)
from tellive.client import LiveClient self.entities = []
self._switches = {}
self._sensors = {}
self._hass = hass self._hass = hass
self._config = config self._config = config
self._client = LiveClient( self._interval = config[DOMAIN].get(CONF_UPDATE_INTERVAL)
public_key=public_key, private_key=private_key, access_token=token, _LOGGER.debug('Update interval %s', self._interval)
access_secret=token_secret)
self._client = Client(public_key,
private_key,
token,
token_secret)
def validate_session(self): def validate_session(self):
"""Make a dummy request to see if the session is valid.""" """Make a request to see if the session is valid."""
response = self.request("user/profile") response = self._client.request_user()
return response and 'email' in response return response and 'email' in response
def discover(self): def update(self, now):
"""Update states, will trigger discover.""" """Periodically poll the servers for current state."""
self.update_sensors() _LOGGER.debug('Updating')
self.update_switches()
def _discover(self, found_devices, component_name):
"""Send discovery event if component not yet discovered."""
if not found_devices:
return
_LOGGER.info("discovered %d new %s devices",
len(found_devices), component_name)
discovery.load_platform(self._hass, component_name, DOMAIN,
found_devices, self._config)
def request(self, what, **params):
"""Send a request to the Tellstick Live API."""
from tellive.live import const
supported_methods = const.TELLSTICK_TURNON \
| const.TELLSTICK_TURNOFF \
| const.TELLSTICK_TOGGLE \
# Tellstick device methods not yet supported
# | const.TELLSTICK_BELL \
# | const.TELLSTICK_DIM \
# | const.TELLSTICK_LEARN \
# | const.TELLSTICK_EXECUTE \
# | const.TELLSTICK_UP \
# | const.TELLSTICK_DOWN \
# | const.TELLSTICK_STOP
default_params = {'supportedMethods': supported_methods,
'includeValues': 1,
'includeScale': 1,
'includeIgnored': 0}
params.update(default_params)
# room for improvement: the telllive library doesn't seem to
# re-use sessions, instead it opens a new session for each request
# this needs to be fixed
try: try:
response = self._client.request(what, params) self._sync()
_LOGGER.debug("got response %s", response) finally:
return response track_point_in_utc_time(self._hass,
except OSError as error: self.update,
_LOGGER.error("failed to make request to Tellduslive servers: %s", now + self._interval)
error)
return None
def update_devices(self, local_devices, remote_devices, component_name): def _sync(self):
"""Update local device list and discover new devices.""" """Update local list of devices."""
if remote_devices is None: self._client.update()
return local_devices
remote_ids = remote_devices.keys() def identify_device(device):
local_ids = local_devices.keys() """Find out what type of HA component to create."""
from tellduslive import (DIM, UP, TURNON)
if device.methods & DIM:
return 'light'
elif device.methods & UP:
return 'cover'
elif device.methods & TURNON:
return 'switch'
else:
_LOGGER.warning('Unidentified device type (methods: %d)',
device.methods)
return 'switch'
added_devices = list(remote_ids - local_ids) def discover(device_id, component):
self._discover(added_devices, """Discover the component."""
component_name) discovery.load_platform(self._hass,
component,
DOMAIN,
[device_id],
self._config)
removed_devices = list(local_ids - remote_ids) known_ids = set([entity.device_id for entity in self.entities])
remote_devices.update({id: dict(local_devices[id], offline=True) for device in self._client.devices:
for id in removed_devices}) if device.device_id in known_ids:
continue
if device.is_sensor:
for item_id in device.items:
discover((device.device_id,) + item_id,
'sensor')
else:
discover(device.device_id,
identify_device(device))
return remote_devices for entity in self.entities:
entity.changed()
def update_sensors(self): def device(self, device_id):
"""Update local list of sensors.""" """Return device representation."""
self._sensors = self.update_devices( import tellduslive
self._sensors, request_sensors(), 'sensor') return tellduslive.Device(self._client, device_id)
def update_switches(self): def is_available(self, device_id):
"""Update local list of switches.""" """Return device availability."""
self._switches = self.update_devices( return device_id in self._client.device_ids
self._switches, request_switches(), 'switch')
def _check_request(self, what, **params):
"""Make request, check result if successful."""
response = self.request(what, **params)
return response and response.get('status') == 'success'
def get_switch(self, switch_id): class TelldusLiveEntity(Entity):
"""Return the switch representation.""" """Base class for all Telldus Live entities."""
return self._switches[switch_id]
def get_sensor(self, sensor_id): def __init__(self, hass, device_id):
"""Return the sensor representation.""" """Initialize the entity."""
return self._sensors[sensor_id] self._id = device_id
self._client = hass.data[DOMAIN]
self._client.entities.append(self)
_LOGGER.debug('Created device %s', self)
def turn_switch_on(self, switch_id): def changed(self):
"""Turn switch off.""" """A property of the device might have changed."""
if self._check_request('device/turnOn', id=switch_id): self.schedule_update_ha_state()
from tellive.live import const
self.get_switch(switch_id)['state'] = const.TELLSTICK_TURNON
def turn_switch_off(self, switch_id): @property
"""Turn switch on.""" def device_id(self):
if self._check_request('device/turnOff', id=switch_id): """Return the id of the device."""
from tellive.live import const return self._id
self.get_switch(switch_id)['state'] = const.TELLSTICK_TURNOFF
@property
def device(self):
"""Return the representaion of the device."""
return self._client.device(self.device_id)
@property
def _state(self):
"""Return the state of the device."""
return self.device.state
@property
def should_poll(self):
"""Polling is not needed."""
return False
@property
def assumed_state(self):
"""Return true if unable to access real state of entity."""
return True
@property
def name(self):
"""Return name of device."""
return self.device.name or DEVICE_DEFAULT_NAME
@property
def available(self):
"""Return true if device is not offline."""
return self._client.is_available(self.device_id)
@property
def device_state_attributes(self):
"""Return the state attributes."""
attrs = {}
if self._battery_level:
attrs[ATTR_BATTERY_LEVEL] = self._battery_level
if self._last_updated:
attrs[ATTR_LAST_UPDATED] = self._last_updated
return attrs
@property
def _battery_level(self):
"""Return the battery level of a device."""
return round(self.device.battery * 100 / 255) \
if self.device.battery else None
@property
def _last_updated(self):
"""Return the last update of a device."""
return str(datetime.fromtimestamp(self.device.last_updated)) \
if self.device.last_updated else None

View File

@ -62,8 +62,6 @@ def setup(hass, config):
from tellcore.library import DirectCallbackDispatcher from tellcore.library import DirectCallbackDispatcher
from tellcore.telldus import TelldusCore from tellcore.telldus import TelldusCore
global TELLCORE_REGISTRY
try: try:
tellcore_lib = TelldusCore( tellcore_lib = TelldusCore(
callback_dispatcher=DirectCallbackDispatcher()) callback_dispatcher=DirectCallbackDispatcher())
@ -75,8 +73,9 @@ def setup(hass, config):
all_tellcore_devices = tellcore_lib.devices() all_tellcore_devices = tellcore_lib.devices()
# Register devices # Register devices
TELLCORE_REGISTRY = TellstickRegistry(hass, tellcore_lib) tellcore_registry = TellstickRegistry(hass, tellcore_lib)
TELLCORE_REGISTRY.register_tellcore_devices(all_tellcore_devices) tellcore_registry.register_tellcore_devices(all_tellcore_devices)
hass.data['tellcore_registry'] = tellcore_registry
# Discover the switches # Discover the switches
_discover(hass, config, 'switch', _discover(hass, config, 'switch',
@ -153,17 +152,17 @@ class TellstickDevice(Entity):
Contains the common logic for all Tellstick devices. Contains the common logic for all Tellstick devices.
""" """
def __init__(self, tellcore_id, signal_repetitions): def __init__(self, tellcore_id, tellcore_registry, signal_repetitions):
"""Initalize the Tellstick device.""" """Initalize the Tellstick device."""
self._signal_repetitions = signal_repetitions self._signal_repetitions = signal_repetitions
self._state = None self._state = None
# Look up our corresponding tellcore device # Look up our corresponding tellcore device
self._tellcore_device = TELLCORE_REGISTRY.get_tellcore_device( self._tellcore_device = tellcore_registry.get_tellcore_device(
tellcore_id) tellcore_id)
# Query tellcore for the current state # Query tellcore for the current state
self.update() self.update()
# Add ourselves to the mapping # Add ourselves to the mapping
TELLCORE_REGISTRY.register_ha_device(tellcore_id, self) tellcore_registry.register_ha_device(tellcore_id, self)
@property @property
def should_poll(self): def should_poll(self):

View File

@ -7,6 +7,7 @@ https://home-assistant.io/components/verisure/
import logging import logging
import threading import threading
import time import time
import os.path
from datetime import timedelta from datetime import timedelta
import voluptuous as vol import voluptuous as vol
@ -14,12 +15,14 @@ import voluptuous as vol
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.helpers import discovery from homeassistant.helpers import discovery
from homeassistant.util import Throttle from homeassistant.util import Throttle
import homeassistant.config as conf_util
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['vsure==0.11.1'] REQUIREMENTS = ['vsure==0.11.1']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
ATTR_DEVICE_SERIAL = 'device_serial'
CONF_ALARM = 'alarm' CONF_ALARM = 'alarm'
CONF_CODE_DIGITS = 'code_digits' CONF_CODE_DIGITS = 'code_digits'
CONF_HYDROMETERS = 'hygrometers' CONF_HYDROMETERS = 'hygrometers'
@ -29,6 +32,7 @@ CONF_SMARTPLUGS = 'smartplugs'
CONF_THERMOMETERS = 'thermometers' CONF_THERMOMETERS = 'thermometers'
CONF_SMARTCAM = 'smartcam' CONF_SMARTCAM = 'smartcam'
DOMAIN = 'verisure' DOMAIN = 'verisure'
SERVICE_CAPTURE_SMARTCAM = 'capture_smartcam'
HUB = None HUB = None
@ -47,6 +51,10 @@ CONFIG_SCHEMA = vol.Schema({
}), }),
}, extra=vol.ALLOW_EXTRA) }, extra=vol.ALLOW_EXTRA)
CAPTURE_IMAGE_SCHEMA = vol.Schema({
vol.Required(ATTR_DEVICE_SERIAL): cv.string
})
def setup(hass, config): def setup(hass, config):
"""Setup the Verisure component.""" """Setup the Verisure component."""
@ -60,6 +68,20 @@ def setup(hass, config):
'camera'): 'camera'):
discovery.load_platform(hass, component, DOMAIN, {}, config) discovery.load_platform(hass, component, DOMAIN, {}, config)
descriptions = conf_util.load_yaml_config_file(
os.path.join(os.path.dirname(__file__), 'services.yaml'))
def capture_smartcam(service):
"""Capture a new picture from a smartcam."""
device_id = service.data.get(ATTR_DEVICE_SERIAL)
HUB.smartcam_capture(device_id)
_LOGGER.debug('Capturing new image from %s', ATTR_DEVICE_SERIAL)
hass.services.register(DOMAIN, SERVICE_CAPTURE_SMARTCAM,
capture_smartcam,
descriptions[DOMAIN][SERVICE_CAPTURE_SMARTCAM],
schema=CAPTURE_IMAGE_SCHEMA)
return True return True
@ -150,6 +172,11 @@ class VerisureHub(object):
self.smartcam_dict = self.my_pages.smartcam.get_imagelist() self.smartcam_dict = self.my_pages.smartcam.get_imagelist()
_LOGGER.debug('New dict: %s', self.smartcam_dict) _LOGGER.debug('New dict: %s', self.smartcam_dict)
@Throttle(timedelta(seconds=30))
def smartcam_capture(self, device_id):
"""Capture a new image from a smartcam."""
self.my_pages.smartcam.capture(device_id)
@property @property
def available(self): def available(self):
"""Return True if hub is available.""" """Return True if hub is available."""

View File

@ -4,6 +4,7 @@ Weather component that handles meteorological data for your location.
For more details about this component, please refer to the documentation at For more details about this component, please refer to the documentation at
https://home-assistant.io/components/weather/ https://home-assistant.io/components/weather/
""" """
import asyncio
import logging import logging
from numbers import Number from numbers import Number
@ -30,11 +31,12 @@ ATTR_WEATHER_WIND_BEARING = 'wind_bearing'
ATTR_WEATHER_WIND_SPEED = 'wind_speed' ATTR_WEATHER_WIND_SPEED = 'wind_speed'
def setup(hass, config): @asyncio.coroutine
def async_setup(hass, config):
"""Setup the weather component.""" """Setup the weather component."""
component = EntityComponent(_LOGGER, DOMAIN, hass) component = EntityComponent(_LOGGER, DOMAIN, hass)
component.setup(config)
yield from component.async_setup(config)
return True return True

View File

@ -15,7 +15,7 @@ from homeassistant.const import (
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['python-wink==0.10.1', 'pubnubsub-handler==0.0.5'] REQUIREMENTS = ['python-wink==0.11.0', 'pubnubsub-handler==0.0.5']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -1,8 +1,8 @@
# coding: utf-8 # coding: utf-8
"""Constants used by Home Assistant components.""" """Constants used by Home Assistant components."""
MAJOR_VERSION = 0 MAJOR_VERSION = 0
MINOR_VERSION = 34 MINOR_VERSION = 35
PATCH_VERSION = '5' PATCH_VERSION = '0.dev0'
__short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION)
__version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION)
REQUIRED_PYTHON_VER = (3, 4, 2) REQUIRED_PYTHON_VER = (3, 4, 2)
@ -278,6 +278,8 @@ ATTR_GPS_ACCURACY = 'gps_accuracy'
ATTR_ASSUMED_STATE = 'assumed_state' ATTR_ASSUMED_STATE = 'assumed_state'
ATTR_STATE = 'state' ATTR_STATE = 'state'
ATTR_OPTION = 'option'
# #### SERVICES #### # #### SERVICES ####
SERVICE_HOMEASSISTANT_STOP = 'stop' SERVICE_HOMEASSISTANT_STOP = 'stop'
SERVICE_HOMEASSISTANT_RESTART = 'restart' SERVICE_HOMEASSISTANT_RESTART = 'restart'
@ -318,6 +320,8 @@ SERVICE_SET_COVER_TILT_POSITION = 'set_cover_tilt_position'
SERVICE_STOP_COVER = 'stop_cover' SERVICE_STOP_COVER = 'stop_cover'
SERVICE_STOP_COVER_TILT = 'stop_cover_tilt' SERVICE_STOP_COVER_TILT = 'stop_cover_tilt'
SERVICE_SELECT_OPTION = 'select_option'
# #### API / REMOTE #### # #### API / REMOTE ####
SERVER_PORT = 8123 SERVER_PORT = 8123

View File

@ -32,7 +32,7 @@ from homeassistant.const import (
SERVICE_CLOSE_COVER, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, SERVICE_CLOSE_COVER, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME,
STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED, STATE_CLOSED, STATE_LOCKED, STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED, STATE_CLOSED, STATE_LOCKED,
STATE_OFF, STATE_ON, STATE_OPEN, STATE_PAUSED, STATE_PLAYING, STATE_OFF, STATE_ON, STATE_OPEN, STATE_PAUSED, STATE_PLAYING,
STATE_UNKNOWN, STATE_UNLOCKED) STATE_UNKNOWN, STATE_UNLOCKED, SERVICE_SELECT_OPTION, ATTR_OPTION)
from homeassistant.core import State from homeassistant.core import State
from homeassistant.util.async import run_coroutine_threadsafe from homeassistant.util.async import run_coroutine_threadsafe
@ -58,7 +58,8 @@ SERVICE_ATTRIBUTES = {
SERVICE_SET_OPERATION_MODE: [ATTR_OPERATION_MODE], SERVICE_SET_OPERATION_MODE: [ATTR_OPERATION_MODE],
SERVICE_SET_AUX_HEAT: [ATTR_AUX_HEAT], SERVICE_SET_AUX_HEAT: [ATTR_AUX_HEAT],
SERVICE_SELECT_SOURCE: [ATTR_INPUT_SOURCE], SERVICE_SELECT_SOURCE: [ATTR_INPUT_SOURCE],
SERVICE_SEND_IR_CODE: [ATTR_IR_CODE] SERVICE_SEND_IR_CODE: [ATTR_IR_CODE],
SERVICE_SELECT_OPTION: [ATTR_OPTION]
} }
# Update this dict when new services are added to HA. # Update this dict when new services are added to HA.
@ -163,11 +164,28 @@ def async_reproduce_state(hass, states, blocking=False):
json.dumps(dict(state.attributes), sort_keys=True)) json.dumps(dict(state.attributes), sort_keys=True))
to_call[key].append(state.entity_id) to_call[key].append(state.entity_id)
domain_tasks = {}
for (service_domain, service, service_data), entity_ids in to_call.items(): for (service_domain, service, service_data), entity_ids in to_call.items():
data = json.loads(service_data) data = json.loads(service_data)
data[ATTR_ENTITY_ID] = entity_ids data[ATTR_ENTITY_ID] = entity_ids
yield from hass.services.async_call(
service_domain, service, data, blocking) if service_domain not in domain_tasks:
domain_tasks[service_domain] = []
domain_tasks[service_domain].append(
hass.services.async_call(service_domain, service, data, blocking)
)
@asyncio.coroutine
def async_handle_service_calls(coro_list):
"""Handle service calls by domain sequence."""
for coro in coro_list:
yield from coro
execute_tasks = [async_handle_service_calls(coro_list)
for coro_list in domain_tasks.values()]
if execute_tasks:
yield from asyncio.wait(execute_tasks, loop=hass.loop)
def state_as_number(state): def state_as_number(state):

View File

@ -84,6 +84,9 @@ directpy==0.1
# homeassistant.components.updater # homeassistant.components.updater
distro==1.0.1 distro==1.0.1
# homeassistant.components.switch.digitalloggers
dlipower==0.7.165
# homeassistant.components.notify.xmpp # homeassistant.components.notify.xmpp
dnspython3==1.15.0 dnspython3==1.15.0
@ -202,7 +205,7 @@ https://github.com/danieljkemp/onkyo-eiscp/archive/python3.zip#onkyo-eiscp==0.9.
# https://github.com/deisi/fritzconnection/archive/b5c14515e1c8e2652b06b6316a7f3913df942841.zip#fritzconnection==0.4.6 # https://github.com/deisi/fritzconnection/archive/b5c14515e1c8e2652b06b6316a7f3913df942841.zip#fritzconnection==0.4.6
# homeassistant.components.netatmo # homeassistant.components.netatmo
https://github.com/jabesq/netatmo-api-python/archive/v0.7.0.zip#lnetatmo==0.7.0 https://github.com/jabesq/netatmo-api-python/archive/v0.8.0.zip#lnetatmo==0.8.0
# homeassistant.components.neato # homeassistant.components.neato
https://github.com/jabesq/pybotvac/archive/v0.0.1.zip#pybotvac==0.0.1 https://github.com/jabesq/pybotvac/archive/v0.0.1.zip#pybotvac==0.0.1
@ -238,6 +241,9 @@ https://github.com/robbiet480/pygtfs/archive/00546724e4bbcb3053110d844ca44e22462
# homeassistant.components.scene.hunterdouglas_powerview # homeassistant.components.scene.hunterdouglas_powerview
https://github.com/sander76/powerviewApi/archive/246e782d60d5c0addcc98d7899a0186f9d5640b0.zip#powerviewApi==0.3.15 https://github.com/sander76/powerviewApi/archive/246e782d60d5c0addcc98d7899a0186f9d5640b0.zip#powerviewApi==0.3.15
# homeassistant.components.binary_sensor.flic
https://github.com/soldag/pyflic/archive/0.4.zip#pyflic==0.4
# homeassistant.components.light.osramlightify # homeassistant.components.light.osramlightify
https://github.com/tfriedel/python-lightify/archive/d6eadcf311e6e21746182d1480e97b350dda2b3e.zip#lightify==1.0.4 https://github.com/tfriedel/python-lightify/archive/d6eadcf311e6e21746182d1480e97b350dda2b3e.zip#lightify==1.0.4
@ -293,7 +299,7 @@ mficlient==0.3.0
miflora==0.1.13 miflora==0.1.13
# homeassistant.components.discovery # homeassistant.components.discovery
netdisco==0.7.7 netdisco==0.8.0
# homeassistant.components.sensor.neurio_energy # homeassistant.components.sensor.neurio_energy
neurio==0.2.10 neurio==0.2.10
@ -384,7 +390,7 @@ pycmus==0.1.0
pydispatcher==2.0.5 pydispatcher==2.0.5
# homeassistant.components.media_player.emby # homeassistant.components.media_player.emby
pyemby==0.1 pyemby==0.2
# homeassistant.components.envisalink # homeassistant.components.envisalink
pyenvisalink==1.9 pyenvisalink==1.9
@ -470,8 +476,14 @@ python-telegram-bot==5.2.0
# homeassistant.components.sensor.twitch # homeassistant.components.sensor.twitch
python-twitch==1.3.0 python-twitch==1.3.0
# homeassistant.components.media_player.vlc
python-vlc==1.1.2
# homeassistant.components.wink # homeassistant.components.wink
python-wink==0.10.1 python-wink==0.11.0
# homeassistant.components.device_tracker.unifi
pyunifi==1.3
# homeassistant.components.keyboard # homeassistant.components.keyboard
# pyuserinput==0.1.11 # pyuserinput==0.1.11
@ -509,6 +521,12 @@ scsgate==0.1.0
# homeassistant.components.notify.sendgrid # homeassistant.components.notify.sendgrid
sendgrid==3.6.3 sendgrid==3.6.3
# homeassistant.components.sensor.sensehat
sense-hat==2.2.0
# homeassistant.components.media_player.aquostv
sharp-aquos-rc==0.2
# homeassistant.components.notify.slack # homeassistant.components.notify.slack
slacker==0.9.30 slacker==0.9.30
@ -542,7 +560,7 @@ steamodd==4.21
tellcore-py==1.1.2 tellcore-py==1.1.2
# homeassistant.components.tellduslive # homeassistant.components.tellduslive
tellive-py==0.5.2 tellduslive==0.1.9
# homeassistant.components.sensor.temper # homeassistant.components.sensor.temper
temperusb==1.5.1 temperusb==1.5.1
@ -560,9 +578,6 @@ twilio==5.4.0
# homeassistant.components.sensor.uber # homeassistant.components.sensor.uber
uber_rides==0.2.7 uber_rides==0.2.7
# homeassistant.components.device_tracker.unifi
unifi==1.2.5
# homeassistant.components.device_tracker.unifi # homeassistant.components.device_tracker.unifi
urllib3 urllib3

View File

@ -42,16 +42,17 @@ class TestAutomationMQTT(unittest.TestCase):
'service': 'test.automation', 'service': 'test.automation',
'data_template': { 'data_template': {
'some': '{{ trigger.platform }} - {{ trigger.topic }}' 'some': '{{ trigger.platform }} - {{ trigger.topic }}'
' - {{ trigger.payload }}' ' - {{ trigger.payload }} - '
'{{ trigger.payload_json.hello }}'
}, },
} }
} }
}) })
fire_mqtt_message(self.hass, 'test-topic', 'test_payload') fire_mqtt_message(self.hass, 'test-topic', '{ "hello": "world" }')
self.hass.block_till_done() self.hass.block_till_done()
self.assertEqual(1, len(self.calls)) self.assertEqual(1, len(self.calls))
self.assertEqual('mqtt - test-topic - test_payload', self.assertEqual('mqtt - test-topic - { "hello": "world" } - world',
self.calls[0].data['some']) self.calls[0].data['some'])
automation.turn_off(self.hass) automation.turn_off(self.hass)

View File

@ -181,34 +181,10 @@ class TestClimateGenericThermostat(unittest.TestCase):
self.assertEqual(SERVICE_TURN_OFF, call.service) self.assertEqual(SERVICE_TURN_OFF, call.service)
self.assertEqual(ENT_SWITCH, call.data['entity_id']) self.assertEqual(ENT_SWITCH, call.data['entity_id'])
def test_set_temp_change_heater_on(self):
"""Test if temperature change turn heater on."""
self._setup_switch(False)
climate.set_temperature(self.hass, 30)
self.hass.block_till_done()
self._setup_sensor(25)
self.hass.block_till_done()
self.assertEqual(1, len(self.calls))
call = self.calls[0]
self.assertEqual('switch', call.domain)
self.assertEqual(SERVICE_TURN_ON, call.service)
self.assertEqual(ENT_SWITCH, call.data['entity_id'])
def test_temp_change_heater_off(self):
"""Test if temperature change turn heater off."""
self._setup_switch(True)
climate.set_temperature(self.hass, 25)
self.hass.block_till_done()
self._setup_sensor(30)
self.hass.block_till_done()
self.assertEqual(1, len(self.calls))
call = self.calls[0]
self.assertEqual('switch', call.domain)
self.assertEqual(SERVICE_TURN_OFF, call.service)
self.assertEqual(ENT_SWITCH, call.data['entity_id'])
def test_temp_change_heater_on_within_tolerance(self): def test_temp_change_heater_on_within_tolerance(self):
"""Test if temperature change turn heater on within tolerance.""" """Test if temperature change doesn't turn heater on within
tolerance.
"""
self._setup_switch(False) self._setup_switch(False)
climate.set_temperature(self.hass, 30) climate.set_temperature(self.hass, 30)
self.hass.block_till_done() self.hass.block_till_done()
@ -217,9 +193,7 @@ class TestClimateGenericThermostat(unittest.TestCase):
self.assertEqual(0, len(self.calls)) self.assertEqual(0, len(self.calls))
def test_temp_change_heater_on_outside_tolerance(self): def test_temp_change_heater_on_outside_tolerance(self):
"""Test if temperature change doesn't turn heater on outside """Test if temperature change turn heater on outside tolerance."""
tolerance.
"""
self._setup_switch(False) self._setup_switch(False)
climate.set_temperature(self.hass, 30) climate.set_temperature(self.hass, 30)
self.hass.block_till_done() self.hass.block_till_done()
@ -231,6 +205,30 @@ class TestClimateGenericThermostat(unittest.TestCase):
self.assertEqual(SERVICE_TURN_ON, call.service) self.assertEqual(SERVICE_TURN_ON, call.service)
self.assertEqual(ENT_SWITCH, call.data['entity_id']) self.assertEqual(ENT_SWITCH, call.data['entity_id'])
def test_temp_change_heater_off_within_tolerance(self):
"""Test if temperature change doesn't turn heater off within
tolerance.
"""
self._setup_switch(True)
climate.set_temperature(self.hass, 30)
self.hass.block_till_done()
self._setup_sensor(31)
self.hass.block_till_done()
self.assertEqual(0, len(self.calls))
def test_temp_change_heater_off_outside_tolerance(self):
"""Test if temperature change turn heater off outside tolerance."""
self._setup_switch(True)
climate.set_temperature(self.hass, 30)
self.hass.block_till_done()
self._setup_sensor(35)
self.hass.block_till_done()
self.assertEqual(1, len(self.calls))
call = self.calls[0]
self.assertEqual('switch', call.domain)
self.assertEqual(SERVICE_TURN_OFF, call.service)
self.assertEqual(ENT_SWITCH, call.data['entity_id'])
def _setup_sensor(self, temp, unit=TEMP_CELSIUS): def _setup_sensor(self, temp, unit=TEMP_CELSIUS):
"""Setup the test sensor.""" """Setup the test sensor."""
self.hass.states.set(ENT_SENSOR, temp, { self.hass.states.set(ENT_SENSOR, temp, {
@ -297,7 +295,18 @@ class TestClimateGenericThermostatACMode(unittest.TestCase):
self.assertEqual(SERVICE_TURN_ON, call.service) self.assertEqual(SERVICE_TURN_ON, call.service)
self.assertEqual(ENT_SWITCH, call.data['entity_id']) self.assertEqual(ENT_SWITCH, call.data['entity_id'])
def test_set_temp_change_ac_off(self): def test_temp_change_ac_off_within_tolerance(self):
"""Test if temperature change doesn't turn ac off within
tolerance.
"""
self._setup_switch(True)
climate.set_temperature(self.hass, 30)
self.hass.block_till_done()
self._setup_sensor(29.8)
self.hass.block_till_done()
self.assertEqual(0, len(self.calls))
def test_set_temp_change_ac_off_outside_tolerance(self):
"""Test if temperature change turn ac off.""" """Test if temperature change turn ac off."""
self._setup_switch(True) self._setup_switch(True)
climate.set_temperature(self.hass, 30) climate.set_temperature(self.hass, 30)
@ -310,7 +319,18 @@ class TestClimateGenericThermostatACMode(unittest.TestCase):
self.assertEqual(SERVICE_TURN_OFF, call.service) self.assertEqual(SERVICE_TURN_OFF, call.service)
self.assertEqual(ENT_SWITCH, call.data['entity_id']) self.assertEqual(ENT_SWITCH, call.data['entity_id'])
def test_temp_change_ac_on(self): def test_temp_change_ac_on_within_tolerance(self):
"""Test if temperature change doesn't turn ac on within
tolerance.
"""
self._setup_switch(False)
climate.set_temperature(self.hass, 25)
self.hass.block_till_done()
self._setup_sensor(25.2)
self.hass.block_till_done()
self.assertEqual(0, len(self.calls))
def test_temp_change_ac_on_outside_tolerance(self):
"""Test if temperature change turn ac on.""" """Test if temperature change turn ac on."""
self._setup_switch(False) self._setup_switch(False)
climate.set_temperature(self.hass, 25) climate.set_temperature(self.hass, 25)

View File

@ -256,6 +256,24 @@ class TestComponentsDeviceTracker(unittest.TestCase):
self.assertEqual(device.vendor, 'unknown') self.assertEqual(device.vendor, 'unknown')
def test_mac_vendor_lookup_on_see(self):
"""Test if macvendor is looked up when device is seen."""
mac = 'B8:27:EB:00:00:00'
vendor_string = 'Raspberry Pi Foundation'
tracker = device_tracker.DeviceTracker(
self.hass, timedelta(seconds=60), 0, [])
with mock_aiohttp_client() as aioclient_mock:
aioclient_mock.get('http://api.macvendors.com/b8:27:eb',
text=vendor_string)
run_coroutine_threadsafe(
tracker.async_see(mac=mac), self.hass.loop).result()
assert aioclient_mock.call_count == 1, \
'No http request for macvendor made!'
self.assertEqual(tracker.devices['b827eb000000'].vendor, vendor_string)
def test_discovery(self): def test_discovery(self):
"""Test discovery.""" """Test discovery."""
scanner = get_component('device_tracker.test').SCANNER scanner = get_component('device_tracker.test').SCANNER
@ -440,6 +458,45 @@ class TestComponentsDeviceTracker(unittest.TestCase):
timedelta(seconds=0)) timedelta(seconds=0))
assert len(config) == 0 assert len(config) == 0
def test_see_state(self):
"""Test device tracker see records state correctly."""
self.assertTrue(setup_component(self.hass, device_tracker.DOMAIN,
TEST_PLATFORM))
params = {
'mac': 'AA:BB:CC:DD:EE:FF',
'dev_id': 'some_device',
'host_name': 'example.com',
'location_name': 'Work',
'gps': [.3, .8],
'gps_accuracy': 1,
'battery': 100,
'attributes': {
'test': 'test',
'number': 1,
},
}
device_tracker.see(self.hass, **params)
self.hass.block_till_done()
config = device_tracker.load_config(self.yaml_devices, self.hass,
timedelta(seconds=0))
assert len(config) == 1
state = self.hass.states.get('device_tracker.examplecom')
attrs = state.attributes
self.assertEqual(state.state, 'Work')
self.assertEqual(state.object_id, 'examplecom')
self.assertEqual(state.name, 'example.com')
self.assertEqual(attrs['friendly_name'], 'example.com')
self.assertEqual(attrs['battery'], 100)
self.assertEqual(attrs['latitude'], 0.3)
self.assertEqual(attrs['longitude'], 0.8)
self.assertEqual(attrs['test'], 'test')
self.assertEqual(attrs['gps_accuracy'], 1)
self.assertEqual(attrs['number'], 1)
@patch('homeassistant.components.device_tracker._LOGGER.warning') @patch('homeassistant.components.device_tracker._LOGGER.warning')
def test_see_failures(self, mock_warning): def test_see_failures(self, mock_warning):
"""Test that the device tracker see failures.""" """Test that the device tracker see failures."""

View File

@ -3,9 +3,10 @@ import unittest
from unittest import mock from unittest import mock
import urllib import urllib
from unifi import controller from pyunifi import controller
import voluptuous as vol import voluptuous as vol
from tests.common import get_test_home_assistant
from homeassistant.components.device_tracker import DOMAIN, unifi as unifi from homeassistant.components.device_tracker import DOMAIN, unifi as unifi
from homeassistant.const import (CONF_HOST, CONF_USERNAME, CONF_PASSWORD, from homeassistant.const import (CONF_HOST, CONF_USERNAME, CONF_PASSWORD,
CONF_PLATFORM) CONF_PLATFORM)
@ -14,6 +15,14 @@ from homeassistant.const import (CONF_HOST, CONF_USERNAME, CONF_PASSWORD,
class TestUnifiScanner(unittest.TestCase): class TestUnifiScanner(unittest.TestCase):
"""Test the Unifiy platform.""" """Test the Unifiy platform."""
def setUp(self):
"""Initialize values for this testcase class."""
self.hass = get_test_home_assistant()
def tearDown(self):
"""Stop everything that was started."""
self.hass.stop()
@mock.patch('homeassistant.components.device_tracker.unifi.UnifiScanner') @mock.patch('homeassistant.components.device_tracker.unifi.UnifiScanner')
@mock.patch.object(controller, 'Controller') @mock.patch.object(controller, 'Controller')
def test_config_minimal(self, mock_ctrl, mock_scanner): def test_config_minimal(self, mock_ctrl, mock_scanner):
@ -25,7 +34,7 @@ class TestUnifiScanner(unittest.TestCase):
CONF_PASSWORD: 'password', CONF_PASSWORD: 'password',
}) })
} }
result = unifi.get_scanner(None, config) result = unifi.get_scanner(self.hass, config)
self.assertEqual(mock_scanner.return_value, result) self.assertEqual(mock_scanner.return_value, result)
self.assertEqual(mock_ctrl.call_count, 1) self.assertEqual(mock_ctrl.call_count, 1)
self.assertEqual( self.assertEqual(
@ -52,7 +61,7 @@ class TestUnifiScanner(unittest.TestCase):
'site_id': 'abcdef01', 'site_id': 'abcdef01',
}) })
} }
result = unifi.get_scanner(None, config) result = unifi.get_scanner(self.hass, config)
self.assertEqual(mock_scanner.return_value, result) self.assertEqual(mock_scanner.return_value, result)
self.assertEqual(mock_ctrl.call_count, 1) self.assertEqual(mock_ctrl.call_count, 1)
self.assertEqual( self.assertEqual(
@ -96,7 +105,7 @@ class TestUnifiScanner(unittest.TestCase):
} }
mock_ctrl.side_effect = urllib.error.HTTPError( mock_ctrl.side_effect = urllib.error.HTTPError(
'/', 500, 'foo', {}, None) '/', 500, 'foo', {}, None)
result = unifi.get_scanner(None, config) result = unifi.get_scanner(self.hass, config)
self.assertFalse(result) self.assertFalse(result)
def test_scanner_update(self): # pylint: disable=no-self-use def test_scanner_update(self): # pylint: disable=no-self-use

View File

@ -5,6 +5,8 @@ import unittest
from homeassistant.const import ( from homeassistant.const import (
STATE_OFF, STATE_ON, STATE_UNKNOWN, STATE_PLAYING, STATE_PAUSED) STATE_OFF, STATE_ON, STATE_UNKNOWN, STATE_PLAYING, STATE_PAUSED)
import homeassistant.components.switch as switch import homeassistant.components.switch as switch
import homeassistant.components.input_slider as input_slider
import homeassistant.components.input_select as input_select
import homeassistant.components.media_player as media_player import homeassistant.components.media_player as media_player
import homeassistant.components.media_player.universal as universal import homeassistant.components.media_player.universal as universal
@ -26,6 +28,7 @@ class MockMediaPlayer(media_player.MediaPlayerDevice):
self._supported_media_commands = 0 self._supported_media_commands = 0
self._source = None self._source = None
self._tracks = 12 self._tracks = 12
self._media_image_url = None
self.service_calls = { self.service_calls = {
'turn_on': mock_service( 'turn_on': mock_service(
@ -90,6 +93,11 @@ class MockMediaPlayer(media_player.MediaPlayerDevice):
"""Supported media commands flag.""" """Supported media commands flag."""
return self._supported_media_commands return self._supported_media_commands
@property
def media_image_url(self):
"""Image url of current playing media."""
return self._media_image_url
def turn_on(self): def turn_on(self):
"""Mock turn_on function.""" """Mock turn_on function."""
self._state = STATE_UNKNOWN self._state = STATE_UNKNOWN
@ -142,6 +150,17 @@ class TestMediaPlayer(unittest.TestCase):
self.mock_state_switch_id = switch.ENTITY_ID_FORMAT.format('state') self.mock_state_switch_id = switch.ENTITY_ID_FORMAT.format('state')
self.hass.states.set(self.mock_state_switch_id, STATE_OFF) self.hass.states.set(self.mock_state_switch_id, STATE_OFF)
self.mock_volume_id = input_slider.ENTITY_ID_FORMAT.format(
'volume_level')
self.hass.states.set(self.mock_volume_id, 0)
self.mock_source_list_id = input_select.ENTITY_ID_FORMAT.format(
'source_list')
self.hass.states.set(self.mock_source_list_id, ['dvd', 'htpc'])
self.mock_source_id = input_select.ENTITY_ID_FORMAT.format('source')
self.hass.states.set(self.mock_source_id, 'dvd')
self.config_children_only = { self.config_children_only = {
'name': 'test', 'platform': 'universal', 'name': 'test', 'platform': 'universal',
'children': [media_player.ENTITY_ID_FORMAT.format('mock1'), 'children': [media_player.ENTITY_ID_FORMAT.format('mock1'),
@ -153,6 +172,9 @@ class TestMediaPlayer(unittest.TestCase):
media_player.ENTITY_ID_FORMAT.format('mock2')], media_player.ENTITY_ID_FORMAT.format('mock2')],
'attributes': { 'attributes': {
'is_volume_muted': self.mock_mute_switch_id, 'is_volume_muted': self.mock_mute_switch_id,
'volume_level': self.mock_volume_id,
'source': self.mock_source_id,
'source_list': self.mock_source_list_id,
'state': self.mock_state_switch_id 'state': self.mock_state_switch_id
} }
} }
@ -384,6 +406,26 @@ class TestMediaPlayer(unittest.TestCase):
ump.update() ump.update()
self.assertEqual(1, ump.volume_level) self.assertEqual(1, ump.volume_level)
def test_media_image_url(self):
"""Test media_image_url property."""
TEST_URL = "test_url"
config = self.config_children_only
universal.validate_config(config)
ump = universal.UniversalMediaPlayer(self.hass, **config)
ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config['name'])
ump.update()
self.assertEqual(None, ump.media_image_url)
self.mock_mp_1._state = STATE_PLAYING
self.mock_mp_1._media_image_url = TEST_URL
self.mock_mp_1.update_ha_state()
ump.update()
# mock_mp_1 will convert the url to the api proxy url. This test
# ensures ump passes through the same url without an additional proxy.
self.assertEqual(self.mock_mp_1.entity_picture, ump.entity_picture)
def test_is_volume_muted_children_only(self): def test_is_volume_muted_children_only(self):
"""Test is volume muted property w/ children only.""" """Test is volume muted property w/ children only."""
config = self.config_children_only config = self.config_children_only
@ -405,6 +447,42 @@ class TestMediaPlayer(unittest.TestCase):
ump.update() ump.update()
self.assertTrue(ump.is_volume_muted) self.assertTrue(ump.is_volume_muted)
def test_source_list_children_and_attr(self):
"""Test source list property w/ children and attrs."""
config = self.config_children_and_attr
universal.validate_config(config)
ump = universal.UniversalMediaPlayer(self.hass, **config)
self.assertEqual("['dvd', 'htpc']", ump.source_list)
self.hass.states.set(self.mock_source_list_id, ['dvd', 'htpc', 'game'])
self.assertEqual("['dvd', 'htpc', 'game']", ump.source_list)
def test_source_children_and_attr(self):
"""Test source property w/ children and attrs."""
config = self.config_children_and_attr
universal.validate_config(config)
ump = universal.UniversalMediaPlayer(self.hass, **config)
self.assertEqual('dvd', ump.source)
self.hass.states.set(self.mock_source_id, 'htpc')
self.assertEqual('htpc', ump.source)
def test_volume_level_children_and_attr(self):
"""Test volume level property w/ children and attrs."""
config = self.config_children_and_attr
universal.validate_config(config)
ump = universal.UniversalMediaPlayer(self.hass, **config)
self.assertEqual('0', ump.volume_level)
self.hass.states.set(self.mock_volume_id, 100)
self.assertEqual('100', ump.volume_level)
def test_is_volume_muted_children_and_attr(self): def test_is_volume_muted_children_and_attr(self):
"""Test is volume muted property w/ children and attrs.""" """Test is volume muted property w/ children and attrs."""
config = self.config_children_and_attr config = self.config_children_and_attr
@ -443,18 +521,20 @@ class TestMediaPlayer(unittest.TestCase):
config['commands']['volume_up'] = 'test' config['commands']['volume_up'] = 'test'
config['commands']['volume_down'] = 'test' config['commands']['volume_down'] = 'test'
config['commands']['volume_mute'] = 'test' config['commands']['volume_mute'] = 'test'
config['commands']['volume_set'] = 'test'
config['commands']['select_source'] = 'test'
ump = universal.UniversalMediaPlayer(self.hass, **config) ump = universal.UniversalMediaPlayer(self.hass, **config)
ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config['name']) ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config['name'])
ump.update() ump.update()
self.mock_mp_1._supported_media_commands = universal.SUPPORT_VOLUME_SET
self.mock_mp_1._state = STATE_PLAYING self.mock_mp_1._state = STATE_PLAYING
self.mock_mp_1.update_ha_state() self.mock_mp_1.update_ha_state()
ump.update() ump.update()
check_flags = universal.SUPPORT_TURN_ON | universal.SUPPORT_TURN_OFF \ check_flags = universal.SUPPORT_TURN_ON | universal.SUPPORT_TURN_OFF \
| universal.SUPPORT_VOLUME_STEP | universal.SUPPORT_VOLUME_MUTE | universal.SUPPORT_VOLUME_STEP | universal.SUPPORT_VOLUME_MUTE \
| universal.SUPPORT_SELECT_SOURCE
self.assertEqual(check_flags, ump.supported_media_commands) self.assertEqual(check_flags, ump.supported_media_commands)

View File

@ -34,16 +34,18 @@ class TestNotifySmtp(unittest.TestCase):
def test_text_email(self): def test_text_email(self):
"""Test build of default text email behavior.""" """Test build of default text email behavior."""
msg = self.mailer.send_message('Test msg') msg = self.mailer.send_message('Test msg')
expected = ('Content-Type: text/plain; charset="us-ascii"\n' expected = ('^Content-Type: text/plain; charset="us-ascii"\n'
'MIME-Version: 1.0\n' 'MIME-Version: 1.0\n'
'Content-Transfer-Encoding: 7bit\n' 'Content-Transfer-Encoding: 7bit\n'
'Subject: Home Assistant\n' 'Subject: Home Assistant\n'
'To: testrecip@test.com\n' 'To: testrecip@test.com\n'
'From: test@test.com\n' 'From: test@test.com\n'
'X-Mailer: HomeAssistant\n' 'X-Mailer: HomeAssistant\n'
'Date: [^\n]+\n'
'Message-Id: <[^@]+@[^>]+>\n'
'\n' '\n'
'Test msg') 'Test msg$')
self.assertEqual(msg, expected) self.assertRegex(msg, expected)
def test_mixed_email(self): def test_mixed_email(self):
"""Test build of mixed text email behavior.""" """Test build of mixed text email behavior."""

View File

@ -9,6 +9,8 @@ from decimal import Decimal
from unittest.mock import Mock from unittest.mock import Mock
from homeassistant.bootstrap import async_setup_component from homeassistant.bootstrap import async_setup_component
from homeassistant.components.sensor.dsmr import DerivativeDSMREntity
from homeassistant.const import STATE_UNKNOWN
from tests.common import assert_setup_component from tests.common import assert_setup_component
@ -62,3 +64,37 @@ def test_default_setup(hass, monkeypatch):
power_tariff = hass.states.get('sensor.power_tariff') power_tariff = hass.states.get('sensor.power_tariff')
assert power_tariff.state == 'low' assert power_tariff.state == 'low'
assert power_tariff.attributes.get('unit_of_measurement') is None assert power_tariff.attributes.get('unit_of_measurement') is None
def test_derivative():
"""Test calculation of derivative value."""
from dsmr_parser.objects import MBusObject
entity = DerivativeDSMREntity('test', '1.0.0')
yield from entity.async_update()
assert entity.state == STATE_UNKNOWN, 'initial state not unknown'
entity.telegram = {
'1.0.0': MBusObject([
{'value': 1},
{'value': 1, 'unit': 'm3'},
])
}
yield from entity.async_update()
assert entity.state == STATE_UNKNOWN, \
'state after first update shoudl still be unknown'
entity.telegram = {
'1.0.0': MBusObject([
{'value': 2},
{'value': 2, 'unit': 'm3'},
])
}
yield from entity.async_update()
assert entity.state == 1, \
'state should be difference between first and second update'
assert entity.unit_of_measurement == 'm3/h'

View File

@ -23,6 +23,16 @@ VALID_CONFIG = {
] ]
} }
INVALID_CONFIG = {
'platform': 'wunderground',
'api_key': 'BOB',
'pws_id': 'bar',
'lang': 'foo',
'monitored_conditions': [
'weather', 'feelslike_c', 'alerts'
]
}
FEELS_LIKE = '40' FEELS_LIKE = '40'
WEATHER = 'Clear' WEATHER = 'Clear'
HTTPS_ICON_URL = 'https://icons.wxug.com/i/c/k/clear.gif' HTTPS_ICON_URL = 'https://icons.wxug.com/i/c/k/clear.gif'
@ -128,17 +138,9 @@ class TestWundergroundSetup(unittest.TestCase):
self.assertTrue( self.assertTrue(
wunderground.setup_platform(self.hass, VALID_CONFIG, wunderground.setup_platform(self.hass, VALID_CONFIG,
self.add_devices, None)) self.add_devices, None))
invalid_config = {
'platform': 'wunderground',
'api_key': 'BOB',
'pws_id': 'bar',
'monitored_conditions': [
'weather', 'feelslike_c', 'alerts'
]
}
self.assertTrue( self.assertTrue(
wunderground.setup_platform(self.hass, invalid_config, wunderground.setup_platform(self.hass, INVALID_CONFIG,
self.add_devices, None)) self.add_devices, None))
@unittest.mock.patch('requests.get', side_effect=mocked_requests_get) @unittest.mock.patch('requests.get', side_effect=mocked_requests_get)

View File

@ -228,6 +228,7 @@ class TestComponentsGroup(unittest.TestCase):
'entities': 'light.Bowl, ' + test_group.entity_id, 'entities': 'light.Bowl, ' + test_group.entity_id,
'icon': 'mdi:work', 'icon': 'mdi:work',
'view': True, 'view': True,
'control': 'hidden',
} }
group_conf['test_group'] = 'hello.world,sensor.happy' group_conf['test_group'] = 'hello.world,sensor.happy'
group_conf['empty_group'] = {'name': 'Empty Group', 'entities': None} group_conf['empty_group'] = {'name': 'Empty Group', 'entities': None}
@ -243,6 +244,8 @@ class TestComponentsGroup(unittest.TestCase):
self.assertEqual('mdi:work', self.assertEqual('mdi:work',
group_state.attributes.get(ATTR_ICON)) group_state.attributes.get(ATTR_ICON))
self.assertTrue(group_state.attributes.get(group.ATTR_VIEW)) self.assertTrue(group_state.attributes.get(group.ATTR_VIEW))
self.assertEqual('hidden',
group_state.attributes.get(group.ATTR_CONTROL))
self.assertTrue(group_state.attributes.get(ATTR_HIDDEN)) self.assertTrue(group_state.attributes.get(ATTR_HIDDEN))
self.assertEqual(1, group_state.attributes.get(group.ATTR_ORDER)) self.assertEqual(1, group_state.attributes.get(group.ATTR_ORDER))
@ -254,6 +257,7 @@ class TestComponentsGroup(unittest.TestCase):
self.assertIsNone(group_state.attributes.get(group.ATTR_AUTO)) self.assertIsNone(group_state.attributes.get(group.ATTR_AUTO))
self.assertIsNone(group_state.attributes.get(ATTR_ICON)) self.assertIsNone(group_state.attributes.get(ATTR_ICON))
self.assertIsNone(group_state.attributes.get(group.ATTR_VIEW)) self.assertIsNone(group_state.attributes.get(group.ATTR_VIEW))
self.assertIsNone(group_state.attributes.get(group.ATTR_CONTROL))
self.assertIsNone(group_state.attributes.get(ATTR_HIDDEN)) self.assertIsNone(group_state.attributes.get(ATTR_HIDDEN))
self.assertEqual(2, group_state.attributes.get(group.ATTR_ORDER)) self.assertEqual(2, group_state.attributes.get(group.ATTR_ORDER))