Merge remote-tracking branch 'refs/remotes/balloob/dev' into dev
This commit is contained in:
commit
bbec34d0e6
92 changed files with 3172 additions and 647 deletions
.coveragerc.travis.ymlMANIFEST.inREADME.md
config
homeassistant
bootstrap.pyconst.pycore.py
requirements_all.txtsetup.pycomponents
alarm_control_panel
api.pyautomation
demo.pydevice_tracker
discovery.pyfrontend
group.pyhttp.pylight
logbook.pymedia_player
mqtt
notify
rfxtrx.pyscene.pysensor
shell_command.pyswitch
thermostat
zone.pyhelpers
util
tests
11
.coveragerc
11
.coveragerc
|
@ -26,6 +26,9 @@ omit =
|
|||
homeassistant/components/zwave.py
|
||||
homeassistant/components/*/zwave.py
|
||||
|
||||
homeassistant/components/rfxtrx.py
|
||||
homeassistant/components/*/rfxtrx.py
|
||||
|
||||
homeassistant/components/ifttt.py
|
||||
homeassistant/components/browser.py
|
||||
homeassistant/components/camera/*
|
||||
|
@ -39,16 +42,20 @@ omit =
|
|||
homeassistant/components/device_tracker/thomson.py
|
||||
homeassistant/components/device_tracker/tomato.py
|
||||
homeassistant/components/device_tracker/tplink.py
|
||||
homeassistant/components/device_tracker/snmp.py
|
||||
homeassistant/components/discovery.py
|
||||
homeassistant/components/downloader.py
|
||||
homeassistant/components/keyboard.py
|
||||
homeassistant/components/light/hue.py
|
||||
homeassistant/components/light/limitlessled.py
|
||||
homeassistant/components/light/blinksticklight.py
|
||||
homeassistant/components/media_player/cast.py
|
||||
homeassistant/components/media_player/denon.py
|
||||
homeassistant/components/media_player/firetv.py
|
||||
homeassistant/components/media_player/itunes.py
|
||||
homeassistant/components/media_player/kodi.py
|
||||
homeassistant/components/media_player/mpd.py
|
||||
homeassistant/components/media_player/plex.py
|
||||
homeassistant/components/media_player/squeezebox.py
|
||||
homeassistant/components/media_player/sonos.py
|
||||
homeassistant/components/notify/file.py
|
||||
|
@ -59,6 +66,7 @@ omit =
|
|||
homeassistant/components/notify/slack.py
|
||||
homeassistant/components/notify/smtp.py
|
||||
homeassistant/components/notify/syslog.py
|
||||
homeassistant/components/notify/telegram.py
|
||||
homeassistant/components/notify/xmpp.py
|
||||
homeassistant/components/sensor/arest.py
|
||||
homeassistant/components/sensor/bitcoin.py
|
||||
|
@ -69,7 +77,7 @@ omit =
|
|||
homeassistant/components/sensor/glances.py
|
||||
homeassistant/components/sensor/mysensors.py
|
||||
homeassistant/components/sensor/openweathermap.py
|
||||
homeassistant/components/sensor/rfxtrx.py
|
||||
homeassistant/components/sensor/rest.py
|
||||
homeassistant/components/sensor/rpi_gpio.py
|
||||
homeassistant/components/sensor/sabnzbd.py
|
||||
homeassistant/components/sensor/swiss_public_transport.py
|
||||
|
@ -77,6 +85,7 @@ omit =
|
|||
homeassistant/components/sensor/temper.py
|
||||
homeassistant/components/sensor/time_date.py
|
||||
homeassistant/components/sensor/transmission.py
|
||||
homeassistant/components/sensor/worldclock.py
|
||||
homeassistant/components/switch/arest.py
|
||||
homeassistant/components/switch/command_switch.py
|
||||
homeassistant/components/switch/edimax.py
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
sudo: false
|
||||
language: python
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/virtualenv/python3.4.2/
|
||||
python:
|
||||
- "3.4"
|
||||
install:
|
||||
|
|
|
@ -1 +1,5 @@
|
|||
recursive-exclude tests *
|
||||
include README.md
|
||||
include LICENSE
|
||||
graft homeassistant
|
||||
prune homeassistant/components/frontend/www_static/home-assistant-polymer
|
||||
recursive-exclude * *.py[co]
|
||||
|
|
12
README.md
12
README.md
|
@ -16,10 +16,12 @@ Check out [the website](https://home-assistant.io) for [a demo][demo], installat
|
|||
|
||||
Examples of devices it can interface it:
|
||||
|
||||
* Monitoring connected devices to a wireless router: [OpenWrt](https://openwrt.org/), [Tomato](http://www.polarcloud.com/tomato), [Netgear](http://netgear.com), [DD-WRT](http://www.dd-wrt.com/site/index), [TPLink](http://www.tp-link.us/), and [ASUSWRT](http://event.asus.com/2013/nw/ASUSWRT/)
|
||||
* Monitoring connected devices to a wireless router: [OpenWrt](https://openwrt.org/), [Tomato](http://www.polarcloud.com/tomato), [Netgear](http://netgear.com), [DD-WRT](http://www.dd-wrt.com/site/index), [TPLink](http://www.tp-link.us/), [ASUSWRT](http://event.asus.com/2013/nw/ASUSWRT/) and any SNMP capable Linksys WAP/WRT
|
||||
*
|
||||
* [Philips Hue](http://meethue.com) lights, [WeMo](http://www.belkin.com/us/Products/home-automation/c/wemo-home-automation/) switches, [Edimax](http://www.edimax.com/) switches, [Efergy](https://efergy.com) energy monitoring, RFXtrx sensors, and [Tellstick](http://www.telldus.se/products/tellstick) devices and sensors
|
||||
* [Google Chromecasts](http://www.google.com/intl/en/chrome/devices/chromecast), [Music Player Daemon](http://www.musicpd.org/), [Logitech Squeezebox](https://en.wikipedia.org/wiki/Squeezebox_%28network_music_player%29), [Kodi (XBMC)](http://kodi.tv/), and iTunes (by way of [itunes-api](https://github.com/maddox/itunes-api))
|
||||
* Support for [ISY994](https://www.universal-devices.com/residential/isy994i-series/) (Insteon and X10 devices), [Z-Wave](http://www.z-wave.com/), [Nest Thermostats](https://nest.com/), [Arduino](https://www.arduino.cc/), [Raspberry Pi](https://www.raspberrypi.org/), and [Modbus](http://www.modbus.org/)
|
||||
* [Google Chromecasts](http://www.google.com/intl/en/chrome/devices/chromecast), [Music Player Daemon](http://www.musicpd.org/), [Logitech Squeezebox](https://en.wikipedia.org/wiki/Squeezebox_%28network_music_player%29), [Plex](https://plex.tv/), [Kodi (XBMC)](http://kodi.tv/), iTunes (by way of [itunes-api](https://github.com/maddox/itunes-api)), and Amazon Fire TV (by way of [python-firetv](https://github.com/happyleavesaoc/python-firetv))
|
||||
* Support for [ISY994](https://www.universal-devices.com/residential/isy994i-series/) (Insteon and X10 devices), [Z-Wave](http://www.z-wave.com/), [Nest Thermostats](https://nest.com/), [RFXtrx](http://www.rfxcom.com/), [Arduino](https://www.arduino.cc/), [Raspberry Pi](https://www.raspberrypi.org/), and [Modbus](http://www.modbus.org/)
|
||||
* Interaction with [IFTTT](https://ifttt.com/)
|
||||
* Integrate data from the [Bitcoin](https://bitcoin.org) network, meteorological data from [OpenWeatherMap](http://openweathermap.org/) and [Forecast.io](https://forecast.io/), [Transmission](http://www.transmissionbt.com/), or [SABnzbd](http://sabnzbd.org).
|
||||
* [See full list of supported devices](https://home-assistant.io/components/)
|
||||
|
||||
|
@ -29,8 +31,8 @@ Built home automation on top of your devices:
|
|||
* Turn on the lights when people get home after sun set
|
||||
* Turn on lights slowly during sun set to compensate for less light
|
||||
* Turn off all lights and devices when everybody leaves the house
|
||||
* Offers a [REST API](https://home-assistant.io/developers/api.html) and can interface with MQTT for easy integration with other projects
|
||||
* Allow sending notifications using [Instapush](https://instapush.im), [Notify My Android (NMA)](http://www.notifymyandroid.com/), [PushBullet](https://www.pushbullet.com/), [PushOver](https://pushover.net/), [Slack](https://slack.com/), and [Jabber (XMPP)](http://xmpp.org)
|
||||
* Offers a [REST API](https://home-assistant.io/developers/api.html) and can interface with MQTT for easy integration with other projects like [OwnTracks](http://owntracks.org/)
|
||||
* Allow sending notifications using [Instapush](https://instapush.im), [Notify My Android (NMA)](http://www.notifymyandroid.com/), [PushBullet](https://www.pushbullet.com/), [PushOver](https://pushover.net/), [Slack](https://slack.com/), [Telegram](https://telegram.org/), and [Jabber (XMPP)](http://xmpp.org)
|
||||
|
||||
The system is built modular so support for other devices or actions can be implemented easily. See also the [section on architecture](https://home-assistant.io/developers/architecture.html) and the [section on creating your own components](https://home-assistant.io/developers/creating_components.html).
|
||||
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
homeassistant:
|
||||
# Omitted values in this section will be auto detected using freegeoip.net
|
||||
|
||||
# Location required to calculate the time the sun rises and sets
|
||||
# Location required to calculate the time the sun rises and sets.
|
||||
# Cooridinates are also used for location for weather related components.
|
||||
# Google Maps can be used to determine more precise GPS cooridinates.
|
||||
latitude: 32.87336
|
||||
longitude: 117.22743
|
||||
|
||||
|
@ -68,11 +70,18 @@ device_sun_light_trigger:
|
|||
|
||||
# A comma separated list of states that have to be tracked as a single group
|
||||
# Grouped states should share the same type of states (ON/OFF or HOME/NOT_HOME)
|
||||
# You can also have groups within groups.
|
||||
group:
|
||||
Home:
|
||||
- group.living_room
|
||||
- group.kitchen
|
||||
living_room:
|
||||
- light.Bowl
|
||||
- light.Ceiling
|
||||
- light.TV_back_light
|
||||
kitchen:
|
||||
- light.fan_bulb_1
|
||||
- light.fan_bulb_2
|
||||
children:
|
||||
- device_tracker.child_1
|
||||
- device_tracker.child_2
|
||||
|
@ -94,28 +103,39 @@ browser:
|
|||
keyboard:
|
||||
|
||||
automation:
|
||||
platform: state
|
||||
alias: Sun starts shining
|
||||
- alias: 'Rule 1 Light on in the evening'
|
||||
trigger:
|
||||
- platform: sun
|
||||
event: sunset
|
||||
offset: "-01:00:00"
|
||||
- platform: state
|
||||
entity_id: group.all_devices
|
||||
state: home
|
||||
condition:
|
||||
- platform: state
|
||||
entity_id: group.all_devices
|
||||
state: home
|
||||
- platform: time
|
||||
after: "16:00:00"
|
||||
before: "23:00:00"
|
||||
action:
|
||||
service: homeassistant.turn_on
|
||||
entity_id: group.living_room
|
||||
|
||||
state_entity_id: sun.sun
|
||||
# Next two are optional, omit to match all
|
||||
state_from: below_horizon
|
||||
state_to: above_horizon
|
||||
- alias: 'Rule 2 - Away Mode'
|
||||
|
||||
execute_service: light.turn_off
|
||||
service_entity_id: group.living_room
|
||||
trigger:
|
||||
- platform: state
|
||||
entity_id: group.all_devices
|
||||
state: 'not_home'
|
||||
|
||||
automation 2:
|
||||
platform: time
|
||||
alias: Beer o Clock
|
||||
condition: use_trigger_values
|
||||
action:
|
||||
service: light.turn_off
|
||||
entity_id: group.all_lights
|
||||
|
||||
time_hours: 16
|
||||
time_minutes: 0
|
||||
time_seconds: 0
|
||||
|
||||
execute_service: notify.notify
|
||||
service_data:
|
||||
message: It's 4, time for beer!
|
||||
# Sensors need to be added into the configuration.yaml as sensor:, sensor 2:, sensor 3:, etc.
|
||||
# Each sensor label should be unique or your sensors might not load correctly.
|
||||
|
||||
sensor:
|
||||
platform: systemmonitor
|
||||
|
@ -135,6 +155,23 @@ sensor:
|
|||
- type: 'process'
|
||||
arg: 'octave-cli'
|
||||
|
||||
sensor 2:
|
||||
platform: forecast
|
||||
api_key: <register on Forecast.io for your PRIVATE API>
|
||||
monitored_conditions:
|
||||
- summary
|
||||
- precip_type
|
||||
- precip_intensity
|
||||
- temperature
|
||||
- dew_point
|
||||
- wind_speed
|
||||
- wind_bearing
|
||||
- cloud_cover
|
||||
- humidity
|
||||
- pressure
|
||||
- visibility
|
||||
- ozone
|
||||
|
||||
script:
|
||||
# Turns on the bedroom lights and then the living room lights 1 minute later
|
||||
wakeup:
|
||||
|
|
|
@ -186,8 +186,8 @@ def from_config_dict(config, hass=None, config_dir=None, enable_log=True,
|
|||
dict, {key: value or {} for key, value in config.items()})
|
||||
|
||||
# Filter out the repeating and common config section [homeassistant]
|
||||
components = (key for key in config.keys()
|
||||
if ' ' not in key and key != core.DOMAIN)
|
||||
components = set(key.split(' ')[0] for key in config.keys()
|
||||
if key != core.DOMAIN)
|
||||
|
||||
if not core_components.setup(hass, config):
|
||||
_LOGGER.error('Home Assistant core failed to initialize. '
|
||||
|
|
|
@ -1,15 +1,18 @@
|
|||
"""
|
||||
homeassistant.components.alarm_control_panel
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Component to interface with a alarm control panel.
|
||||
"""
|
||||
import logging
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
import os
|
||||
|
||||
from homeassistant.components import verisure
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
SERVICE_ALARM_DISARM, SERVICE_ALARM_ARM_HOME, SERVICE_ALARM_ARM_AWAY)
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
|
||||
DOMAIN = 'alarm_control_panel'
|
||||
DEPENDENCIES = []
|
||||
|
@ -29,9 +32,11 @@ SERVICE_TO_METHOD = {
|
|||
}
|
||||
|
||||
ATTR_CODE = 'code'
|
||||
ATTR_CODE_FORMAT = 'code_format'
|
||||
|
||||
ATTR_TO_PROPERTY = [
|
||||
ATTR_CODE,
|
||||
ATTR_CODE_FORMAT
|
||||
]
|
||||
|
||||
|
||||
|
@ -57,8 +62,12 @@ def setup(hass, config):
|
|||
for alarm in target_alarms:
|
||||
getattr(alarm, method)(code)
|
||||
|
||||
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)
|
||||
hass.services.register(DOMAIN, service, alarm_service_handler,
|
||||
descriptions.get(service))
|
||||
|
||||
return True
|
||||
|
||||
|
@ -93,16 +102,31 @@ def alarm_arm_away(hass, code, entity_id=None):
|
|||
hass.services.call(DOMAIN, SERVICE_ALARM_ARM_AWAY, data)
|
||||
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
class AlarmControlPanel(Entity):
|
||||
""" ABC for alarm control devices. """
|
||||
def alarm_disarm(self, code):
|
||||
|
||||
@property
|
||||
def code_format(self):
|
||||
""" regex for code format or None if no code is required. """
|
||||
return None
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
""" Send disarm command. """
|
||||
raise NotImplementedError()
|
||||
|
||||
def alarm_arm_home(self, code):
|
||||
def alarm_arm_home(self, code=None):
|
||||
""" Send arm home command. """
|
||||
raise NotImplementedError()
|
||||
|
||||
def alarm_arm_away(self, code):
|
||||
def alarm_arm_away(self, code=None):
|
||||
""" Send arm away command. """
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
""" Return the state attributes. """
|
||||
state_attr = {
|
||||
ATTR_CODE_FORMAT: self.code_format,
|
||||
}
|
||||
return state_attr
|
||||
|
|
167
homeassistant/components/alarm_control_panel/mqtt.py
Normal file
167
homeassistant/components/alarm_control_panel/mqtt.py
Normal file
|
@ -0,0 +1,167 @@
|
|||
"""
|
||||
homeassistant.components.alarm_control_panel.mqtt
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
This platform enables the possibility to control a MQTT alarm.
|
||||
In this platform, 'state_topic' and 'command_topic' are required.
|
||||
The alarm will only change state after receiving the a new state
|
||||
from 'state_topic'. If these messages are published with RETAIN flag,
|
||||
the MQTT alarm will receive an instant state update after subscription
|
||||
and will start with correct state. Otherwise, the initial state will
|
||||
be 'unknown'.
|
||||
|
||||
Configuration:
|
||||
|
||||
alarm_control_panel:
|
||||
platform: mqtt
|
||||
name: "MQTT Alarm"
|
||||
state_topic: "home/alarm"
|
||||
command_topic: "home/alarm/set"
|
||||
qos: 0
|
||||
payload_disarm: "DISARM"
|
||||
payload_arm_home: "ARM_HOME"
|
||||
payload_arm_away: "ARM_AWAY"
|
||||
code: "mySecretCode"
|
||||
|
||||
Variables:
|
||||
|
||||
name
|
||||
*Optional
|
||||
The name of the alarm. Default is 'MQTT Alarm'.
|
||||
|
||||
state_topic
|
||||
*Required
|
||||
The MQTT topic subscribed to receive state updates.
|
||||
|
||||
command_topic
|
||||
*Required
|
||||
The MQTT topic to publish commands to change the alarm state.
|
||||
|
||||
qos
|
||||
*Optional
|
||||
The maximum QoS level of the state topic. Default is 0.
|
||||
This QoS will also be used to publishing messages.
|
||||
|
||||
payload_disarm
|
||||
*Optional
|
||||
The payload do disarm alarm. Default is "DISARM".
|
||||
|
||||
payload_arm_home
|
||||
*Optional
|
||||
The payload to set armed-home mode. Default is "ARM_HOME".
|
||||
|
||||
payload_arm_away
|
||||
*Optional
|
||||
The payload to set armed-away mode. Default is "ARM_AWAY".
|
||||
|
||||
code
|
||||
*Optional
|
||||
If defined, specifies a code to enable or disable the alarm in the frontend.
|
||||
"""
|
||||
import logging
|
||||
import homeassistant.components.mqtt as mqtt
|
||||
import homeassistant.components.alarm_control_panel as alarm
|
||||
|
||||
from homeassistant.const import (STATE_UNKNOWN)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_NAME = "MQTT Alarm"
|
||||
DEFAULT_QOS = 0
|
||||
DEFAULT_PAYLOAD_DISARM = "DISARM"
|
||||
DEFAULT_PAYLOAD_ARM_HOME = "ARM_HOME"
|
||||
DEFAULT_PAYLOAD_ARM_AWAY = "ARM_AWAY"
|
||||
|
||||
DEPENDENCIES = ['mqtt']
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
""" Sets up the MQTT platform. """
|
||||
|
||||
if config.get('state_topic') is None:
|
||||
_LOGGER.error("Missing required variable: state_topic")
|
||||
return False
|
||||
|
||||
if config.get('command_topic') is None:
|
||||
_LOGGER.error("Missing required variable: command_topic")
|
||||
return False
|
||||
|
||||
add_devices([MqttAlarm(
|
||||
hass,
|
||||
config.get('name', DEFAULT_NAME),
|
||||
config.get('state_topic'),
|
||||
config.get('command_topic'),
|
||||
config.get('qos', DEFAULT_QOS),
|
||||
config.get('payload_disarm', DEFAULT_PAYLOAD_DISARM),
|
||||
config.get('payload_arm_home', DEFAULT_PAYLOAD_ARM_HOME),
|
||||
config.get('payload_arm_away', DEFAULT_PAYLOAD_ARM_AWAY),
|
||||
config.get('code'))])
|
||||
|
||||
|
||||
# pylint: disable=too-many-arguments, too-many-instance-attributes
|
||||
class MqttAlarm(alarm.AlarmControlPanel):
|
||||
""" represents a MQTT alarm status within home assistant. """
|
||||
|
||||
def __init__(self, hass, name, state_topic, command_topic, qos,
|
||||
payload_disarm, payload_arm_home, payload_arm_away, code):
|
||||
self._state = STATE_UNKNOWN
|
||||
self._hass = hass
|
||||
self._name = name
|
||||
self._state_topic = state_topic
|
||||
self._command_topic = command_topic
|
||||
self._qos = qos
|
||||
self._payload_disarm = payload_disarm
|
||||
self._payload_arm_home = payload_arm_home
|
||||
self._payload_arm_away = payload_arm_away
|
||||
self._code = code
|
||||
|
||||
def message_received(topic, payload, qos):
|
||||
""" A new MQTT message has been received. """
|
||||
self._state = payload
|
||||
self.update_ha_state()
|
||||
|
||||
mqtt.subscribe(hass, self._state_topic, message_received, self._qos)
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
""" No polling needed """
|
||||
return False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" Returns the name of the device. """
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
""" Returns the state of the device. """
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def code_format(self):
|
||||
""" One or more characters if code is defined """
|
||||
return None if self._code is None else '.+'
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
""" Send disarm command. """
|
||||
if code == str(self._code) or self.code_format is None:
|
||||
mqtt.publish(self.hass, self._command_topic,
|
||||
self._payload_disarm, self._qos)
|
||||
else:
|
||||
_LOGGER.warning("Wrong code entered while disarming!")
|
||||
|
||||
def alarm_arm_home(self, code=None):
|
||||
""" Send arm home command. """
|
||||
if code == str(self._code) or self.code_format is None:
|
||||
mqtt.publish(self.hass, self._command_topic,
|
||||
self._payload_arm_home, self._qos)
|
||||
else:
|
||||
_LOGGER.warning("Wrong code entered while arming home!")
|
||||
|
||||
def alarm_arm_away(self, code=None):
|
||||
""" Send arm away command. """
|
||||
if code == str(self._code) or self.code_format is None:
|
||||
mqtt.publish(self.hass, self._command_topic,
|
||||
self._payload_arm_away, self._qos)
|
||||
else:
|
||||
_LOGGER.warning("Wrong code entered while arming away!")
|
|
@ -1,6 +1,6 @@
|
|||
"""
|
||||
homeassistant.components.alarm_control_panel.verisure
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Interfaces with Verisure alarm control panel.
|
||||
"""
|
||||
import logging
|
||||
|
@ -34,7 +34,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
|
||||
|
||||
class VerisureAlarm(alarm.AlarmControlPanel):
|
||||
""" represents a Verisure alarm status within home assistant. """
|
||||
""" Represents a Verisure alarm status. """
|
||||
|
||||
def __init__(self, alarm_status):
|
||||
self._id = alarm_status.id
|
||||
|
@ -51,8 +51,13 @@ class VerisureAlarm(alarm.AlarmControlPanel):
|
|||
""" Returns the state of the device. """
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def code_format(self):
|
||||
""" Four digit code required. """
|
||||
return '^\\d{4}$'
|
||||
|
||||
def update(self):
|
||||
''' update alarm status '''
|
||||
""" Update alarm status """
|
||||
verisure.update()
|
||||
|
||||
if verisure.STATUS[self._device][self._id].status == 'unarmed':
|
||||
|
@ -66,21 +71,21 @@ class VerisureAlarm(alarm.AlarmControlPanel):
|
|||
'Unknown alarm state %s',
|
||||
verisure.STATUS[self._device][self._id].status)
|
||||
|
||||
def alarm_disarm(self, code):
|
||||
def alarm_disarm(self, code=None):
|
||||
""" Send disarm command. """
|
||||
verisure.MY_PAGES.set_alarm_status(
|
||||
code,
|
||||
verisure.MY_PAGES.ALARM_DISARMED)
|
||||
_LOGGER.warning('disarming')
|
||||
|
||||
def alarm_arm_home(self, code):
|
||||
def alarm_arm_home(self, code=None):
|
||||
""" Send arm home command. """
|
||||
verisure.MY_PAGES.set_alarm_status(
|
||||
code,
|
||||
verisure.MY_PAGES.ALARM_ARMED_HOME)
|
||||
_LOGGER.warning('arming home')
|
||||
|
||||
def alarm_arm_away(self, code):
|
||||
def alarm_arm_away(self, code=None):
|
||||
""" Send arm away command. """
|
||||
verisure.MY_PAGES.set_alarm_status(
|
||||
code,
|
||||
|
|
|
@ -103,6 +103,10 @@ def _handle_get_api_stream(handler, path_match, data):
|
|||
write_lock = threading.Lock()
|
||||
block = threading.Event()
|
||||
|
||||
restrict = data.get('restrict')
|
||||
if restrict:
|
||||
restrict = restrict.split(',')
|
||||
|
||||
def write_message(payload):
|
||||
""" Writes a message to the output. """
|
||||
with write_lock:
|
||||
|
@ -118,7 +122,8 @@ def _handle_get_api_stream(handler, path_match, data):
|
|||
""" Forwards events to the open request. """
|
||||
nonlocal gracefully_closed
|
||||
|
||||
if block.is_set() or event.event_type == EVENT_TIME_CHANGED:
|
||||
if block.is_set() or event.event_type == EVENT_TIME_CHANGED or \
|
||||
restrict and event.event_type not in restrict:
|
||||
return
|
||||
elif event.event_type == EVENT_HOMEASSISTANT_STOP:
|
||||
gracefully_closed = True
|
||||
|
|
|
@ -28,6 +28,11 @@ def trigger(hass, config, action):
|
|||
from_state = config.get(CONF_FROM, MATCH_ALL)
|
||||
to_state = config.get(CONF_TO) or config.get(CONF_STATE) or MATCH_ALL
|
||||
|
||||
if isinstance(from_state, bool) or isinstance(to_state, bool):
|
||||
logging.getLogger(__name__).error(
|
||||
'Config error. Surround to/from values with quotes.')
|
||||
return False
|
||||
|
||||
def state_automation_listener(entity, from_s, to_s):
|
||||
""" Listens for state changes and calls action. """
|
||||
action()
|
||||
|
|
85
homeassistant/components/automation/zone.py
Normal file
85
homeassistant/components/automation/zone.py
Normal file
|
@ -0,0 +1,85 @@
|
|||
"""
|
||||
homeassistant.components.automation.zone
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Offers zone automation rules.
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components import zone
|
||||
from homeassistant.helpers.event import track_state_change
|
||||
from homeassistant.const import (
|
||||
ATTR_GPS_ACCURACY, ATTR_LATITUDE, ATTR_LONGITUDE, MATCH_ALL)
|
||||
|
||||
|
||||
CONF_ENTITY_ID = "entity_id"
|
||||
CONF_ZONE = "zone"
|
||||
CONF_EVENT = "event"
|
||||
EVENT_ENTER = "enter"
|
||||
EVENT_LEAVE = "leave"
|
||||
DEFAULT_EVENT = EVENT_ENTER
|
||||
|
||||
|
||||
def trigger(hass, config, action):
|
||||
""" Listen for state changes based on `config`. """
|
||||
entity_id = config.get(CONF_ENTITY_ID)
|
||||
zone_entity_id = config.get(CONF_ZONE)
|
||||
|
||||
if entity_id is None or zone_entity_id is None:
|
||||
logging.getLogger(__name__).error(
|
||||
"Missing trigger configuration key %s or %s", CONF_ENTITY_ID,
|
||||
CONF_ZONE)
|
||||
return False
|
||||
|
||||
event = config.get(CONF_EVENT, DEFAULT_EVENT)
|
||||
|
||||
def zone_automation_listener(entity, from_s, to_s):
|
||||
""" Listens for state changes and calls action. """
|
||||
if from_s and None in (from_s.attributes.get(ATTR_LATITUDE),
|
||||
from_s.attributes.get(ATTR_LONGITUDE)) or \
|
||||
None in (to_s.attributes.get(ATTR_LATITUDE),
|
||||
to_s.attributes.get(ATTR_LONGITUDE)):
|
||||
return
|
||||
|
||||
from_match = _in_zone(hass, zone_entity_id, from_s) if from_s else None
|
||||
to_match = _in_zone(hass, zone_entity_id, to_s)
|
||||
|
||||
if event == EVENT_ENTER and not from_match and to_match or \
|
||||
event == EVENT_LEAVE and from_match and not to_match:
|
||||
action()
|
||||
|
||||
track_state_change(
|
||||
hass, entity_id, zone_automation_listener, MATCH_ALL, MATCH_ALL)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def if_action(hass, config):
|
||||
""" Wraps action method with zone based condition. """
|
||||
entity_id = config.get(CONF_ENTITY_ID)
|
||||
zone_entity_id = config.get(CONF_ZONE)
|
||||
|
||||
if entity_id is None or zone_entity_id is None:
|
||||
logging.getLogger(__name__).error(
|
||||
"Missing condition configuration key %s or %s", CONF_ENTITY_ID,
|
||||
CONF_ZONE)
|
||||
return False
|
||||
|
||||
def if_in_zone():
|
||||
""" Test if condition. """
|
||||
return _in_zone(hass, zone_entity_id, hass.states.get(entity_id))
|
||||
|
||||
return if_in_zone
|
||||
|
||||
|
||||
def _in_zone(hass, zone_entity_id, state):
|
||||
""" Check if state is in zone. """
|
||||
if not state or None in (state.attributes.get(ATTR_LATITUDE),
|
||||
state.attributes.get(ATTR_LONGITUDE)):
|
||||
return False
|
||||
|
||||
zone_state = hass.states.get(zone_entity_id)
|
||||
return zone_state and zone.in_zone(
|
||||
zone_state, state.attributes.get(ATTR_LATITUDE),
|
||||
state.attributes.get(ATTR_LONGITUDE),
|
||||
state.attributes.get(ATTR_GPS_ACCURACY, 0))
|
|
@ -33,10 +33,10 @@ def setup(hass, config):
|
|||
|
||||
# Setup sun
|
||||
if not hass.config.latitude:
|
||||
hass.config.latitude = '32.87336'
|
||||
hass.config.latitude = 32.87336
|
||||
|
||||
if not hass.config.longitude:
|
||||
hass.config.longitude = '117.22743'
|
||||
hass.config.longitude = 117.22743
|
||||
|
||||
bootstrap.setup_component(hass, 'sun')
|
||||
|
||||
|
@ -60,7 +60,7 @@ def setup(hass, config):
|
|||
{'camera': {
|
||||
'platform': 'generic',
|
||||
'name': 'IP Camera',
|
||||
'still_image_url': 'http://194.218.96.92/jpg/image.jpg',
|
||||
'still_image_url': 'http://home-assistant.io/demo/webcam.jpg',
|
||||
}})
|
||||
|
||||
# Setup scripts
|
||||
|
@ -108,7 +108,9 @@ def setup(hass, config):
|
|||
"http://graph.facebook.com/297400035/picture",
|
||||
ATTR_FRIENDLY_NAME: 'Paulus'})
|
||||
hass.states.set("device_tracker.anne_therese", "not_home",
|
||||
{ATTR_FRIENDLY_NAME: 'Anne Therese'})
|
||||
{ATTR_FRIENDLY_NAME: 'Anne Therese',
|
||||
'latitude': hass.config.latitude + 0.002,
|
||||
'longitude': hass.config.longitude + 0.002})
|
||||
|
||||
hass.states.set("group.all_devices", "home",
|
||||
{
|
||||
|
|
|
@ -30,7 +30,7 @@ import os
|
|||
import threading
|
||||
|
||||
from homeassistant.bootstrap import prepare_setup_platform
|
||||
from homeassistant.components import discovery, group
|
||||
from homeassistant.components import discovery, group, zone
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import config_per_platform
|
||||
|
@ -40,10 +40,11 @@ import homeassistant.util.dt as dt_util
|
|||
|
||||
from homeassistant.helpers.event import track_utc_time_change
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_PICTURE, DEVICE_DEFAULT_NAME, STATE_HOME, STATE_NOT_HOME)
|
||||
ATTR_ENTITY_PICTURE, ATTR_GPS_ACCURACY, ATTR_LATITUDE, ATTR_LONGITUDE,
|
||||
DEVICE_DEFAULT_NAME, STATE_HOME, STATE_NOT_HOME)
|
||||
|
||||
DOMAIN = "device_tracker"
|
||||
DEPENDENCIES = []
|
||||
DEPENDENCIES = ['zone']
|
||||
|
||||
GROUP_NAME_ALL_DEVICES = 'all devices'
|
||||
ENTITY_ID_ALL_DEVICES = group.ENTITY_ID_FORMAT.format('all_devices')
|
||||
|
@ -70,14 +71,11 @@ DEFAULT_HOME_RANGE = 100
|
|||
|
||||
SERVICE_SEE = 'see'
|
||||
|
||||
ATTR_LATITUDE = 'latitude'
|
||||
ATTR_LONGITUDE = 'longitude'
|
||||
ATTR_MAC = 'mac'
|
||||
ATTR_DEV_ID = 'dev_id'
|
||||
ATTR_HOST_NAME = 'host_name'
|
||||
ATTR_LOCATION_NAME = 'location_name'
|
||||
ATTR_GPS = 'gps'
|
||||
ATTR_GPS_ACCURACY = 'gps_accuracy'
|
||||
ATTR_BATTERY = 'battery'
|
||||
|
||||
DISCOVERY_PLATFORMS = {
|
||||
|
@ -116,6 +114,8 @@ def setup(hass, config):
|
|||
os.remove(csv_path)
|
||||
|
||||
conf = config.get(DOMAIN, {})
|
||||
if isinstance(conf, list):
|
||||
conf = conf[0]
|
||||
consider_home = timedelta(
|
||||
seconds=util.convert(conf.get(CONF_CONSIDER_HOME), int,
|
||||
DEFAULT_CONSIDER_HOME))
|
||||
|
@ -175,7 +175,10 @@ def setup(hass, config):
|
|||
ATTR_GPS, ATTR_GPS_ACCURACY, ATTR_BATTERY)}
|
||||
tracker.see(**args)
|
||||
|
||||
hass.services.register(DOMAIN, SERVICE_SEE, see_service)
|
||||
descriptions = load_yaml_config_file(
|
||||
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
||||
hass.services.register(DOMAIN, SERVICE_SEE, see_service,
|
||||
descriptions.get(SERVICE_SEE))
|
||||
|
||||
return True
|
||||
|
||||
|
@ -338,7 +341,7 @@ class Device(Entity):
|
|||
self.last_seen = dt_util.utcnow()
|
||||
self.host_name = host_name
|
||||
self.location_name = location_name
|
||||
self.gps_accuracy = gps_accuracy
|
||||
self.gps_accuracy = gps_accuracy or 0
|
||||
self.battery = battery
|
||||
if gps is None:
|
||||
self.gps = None
|
||||
|
@ -363,7 +366,15 @@ class Device(Entity):
|
|||
elif self.location_name:
|
||||
self._state = self.location_name
|
||||
elif self.gps is not None:
|
||||
self._state = STATE_HOME if self.gps_home else STATE_NOT_HOME
|
||||
zone_state = zone.active_zone(self.hass, self.gps[0], self.gps[1],
|
||||
self.gps_accuracy)
|
||||
if zone_state is None:
|
||||
self._state = STATE_NOT_HOME
|
||||
elif zone_state.entity_id == zone.ENTITY_ID_HOME:
|
||||
self._state = STATE_HOME
|
||||
else:
|
||||
self._state = zone_state.name
|
||||
|
||||
elif self.stale():
|
||||
self._state = STATE_NOT_HOME
|
||||
self.last_update_home = False
|
||||
|
|
|
@ -161,9 +161,10 @@ class AsusWrtDeviceScanner(object):
|
|||
# For leases where the client doesn't set a hostname, ensure
|
||||
# it is blank and not '*', which breaks the entity_id down
|
||||
# the line
|
||||
host = match.group('host')
|
||||
if host == '*':
|
||||
host = ''
|
||||
if match:
|
||||
host = match.group('host')
|
||||
if host == '*':
|
||||
host = ''
|
||||
|
||||
devices[match.group('ip')] = {
|
||||
'host': host,
|
||||
|
@ -174,6 +175,6 @@ class AsusWrtDeviceScanner(object):
|
|||
|
||||
for neighbor in neighbors:
|
||||
match = _IP_NEIGH_REGEX.search(neighbor.decode('utf-8'))
|
||||
if match.group('ip') in devices:
|
||||
if match and match.group('ip') in devices:
|
||||
devices[match.group('ip')]['status'] = match.group('status')
|
||||
return devices
|
||||
|
|
|
@ -46,6 +46,7 @@ MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
|
|||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
_DDWRT_DATA_REGEX = re.compile(r'\{(\w+)::([^\}]*)\}')
|
||||
_MAC_REGEX = re.compile(r'(([0-9A-Fa-f]{1,2}\:){5}[0-9A-Fa-f]{1,2})')
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
|
@ -77,7 +78,7 @@ class DdWrtDeviceScanner(object):
|
|||
|
||||
self.last_results = {}
|
||||
|
||||
self.mac2name = None
|
||||
self.mac2name = {}
|
||||
|
||||
# Test the router is accessible
|
||||
url = 'http://{}/Status_Wireless.live.asp'.format(self.host)
|
||||
|
@ -98,30 +99,33 @@ class DdWrtDeviceScanner(object):
|
|||
|
||||
with self.lock:
|
||||
# if not initialised and not already scanned and not found
|
||||
if self.mac2name is None or device not in self.mac2name:
|
||||
if device not in self.mac2name:
|
||||
url = 'http://{}/Status_Lan.live.asp'.format(self.host)
|
||||
data = self.get_ddwrt_data(url)
|
||||
|
||||
if not data:
|
||||
return
|
||||
return None
|
||||
|
||||
dhcp_leases = data.get('dhcp_leases', None)
|
||||
if dhcp_leases:
|
||||
# remove leading and trailing single quotes
|
||||
cleaned_str = dhcp_leases.strip().strip('"')
|
||||
elements = cleaned_str.split('","')
|
||||
num_clients = int(len(elements)/5)
|
||||
self.mac2name = {}
|
||||
for idx in range(0, num_clients):
|
||||
# this is stupid but the data is a single array
|
||||
# every 5 elements represents one hosts, the MAC
|
||||
# is the third element and the name is the first
|
||||
mac_index = (idx * 5) + 2
|
||||
if mac_index < len(elements):
|
||||
mac = elements[mac_index]
|
||||
self.mac2name[mac] = elements[idx * 5]
|
||||
|
||||
return self.mac2name.get(device, None)
|
||||
if not dhcp_leases:
|
||||
return None
|
||||
|
||||
# remove leading and trailing single quotes
|
||||
cleaned_str = dhcp_leases.strip().strip('"')
|
||||
elements = cleaned_str.split('","')
|
||||
num_clients = int(len(elements)/5)
|
||||
self.mac2name = {}
|
||||
for idx in range(0, num_clients):
|
||||
# this is stupid but the data is a single array
|
||||
# every 5 elements represents one hosts, the MAC
|
||||
# is the third element and the name is the first
|
||||
mac_index = (idx * 5) + 2
|
||||
if mac_index < len(elements):
|
||||
mac = elements[mac_index]
|
||||
self.mac2name[mac] = elements[idx * 5]
|
||||
|
||||
return self.mac2name.get(device)
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
||||
def _update_info(self):
|
||||
|
@ -141,29 +145,25 @@ class DdWrtDeviceScanner(object):
|
|||
if not data:
|
||||
return False
|
||||
|
||||
if data:
|
||||
self.last_results = []
|
||||
active_clients = data.get('active_wireless', None)
|
||||
if active_clients:
|
||||
# This is really lame, instead of using JSON the DD-WRT UI
|
||||
# uses its own data format for some reason and then
|
||||
# regex's out values so I guess I have to do the same,
|
||||
# LAME!!!
|
||||
self.last_results = []
|
||||
|
||||
# remove leading and trailing single quotes
|
||||
clean_str = active_clients.strip().strip("'")
|
||||
elements = clean_str.split("','")
|
||||
active_clients = data.get('active_wireless', None)
|
||||
if not active_clients:
|
||||
return False
|
||||
|
||||
num_clients = int(len(elements)/9)
|
||||
for idx in range(0, num_clients):
|
||||
# get every 9th element which is the MAC address
|
||||
index = idx * 9
|
||||
if index < len(elements):
|
||||
self.last_results.append(elements[index])
|
||||
# This is really lame, instead of using JSON the DD-WRT UI
|
||||
# uses its own data format for some reason and then
|
||||
# regex's out values so I guess I have to do the same,
|
||||
# LAME!!!
|
||||
|
||||
return True
|
||||
# remove leading and trailing single quotes
|
||||
clean_str = active_clients.strip().strip("'")
|
||||
elements = clean_str.split("','")
|
||||
|
||||
return False
|
||||
self.last_results.extend(item for item in elements
|
||||
if _MAC_REGEX.match(item))
|
||||
|
||||
return True
|
||||
|
||||
def get_ddwrt_data(self, url):
|
||||
""" Retrieve data from DD-WRT and return parsed result. """
|
||||
|
|
|
@ -117,15 +117,18 @@ class NmapDeviceScanner(object):
|
|||
scanner = PortScanner()
|
||||
|
||||
options = "-F --host-timeout 5"
|
||||
exclude_targets = set()
|
||||
|
||||
if self.home_interval:
|
||||
now = dt_util.now()
|
||||
for host in self.last_results:
|
||||
if host.last_update + self.home_interval > now:
|
||||
exclude_targets.add(host)
|
||||
if len(exclude_targets) > 0:
|
||||
target_list = [t.ip for t in exclude_targets]
|
||||
options += " --exclude {}".format(",".join(target_list))
|
||||
boundary = dt_util.now() - self.home_interval
|
||||
last_results = [device for device in self.last_results
|
||||
if device.last_update > boundary]
|
||||
if last_results:
|
||||
# Pylint is confused here.
|
||||
# pylint: disable=no-member
|
||||
options += " --exclude {}".format(",".join(device.ip for device
|
||||
in last_results))
|
||||
else:
|
||||
last_results = []
|
||||
|
||||
try:
|
||||
result = scanner.scan(hosts=self.hosts, arguments=options)
|
||||
|
@ -133,18 +136,17 @@ class NmapDeviceScanner(object):
|
|||
return False
|
||||
|
||||
now = dt_util.now()
|
||||
self.last_results = []
|
||||
for ipv4, info in result['scan'].items():
|
||||
if info['status']['state'] != 'up':
|
||||
continue
|
||||
name = info['hostnames'][0] if info['hostnames'] else ipv4
|
||||
name = info['hostnames'][0]['name'] if info['hostnames'] else ipv4
|
||||
# Mac address only returned if nmap ran as root
|
||||
mac = info['addresses'].get('mac') or _arp(ipv4)
|
||||
if mac is None:
|
||||
continue
|
||||
device = Device(mac.upper(), name, ipv4, now)
|
||||
self.last_results.append(device)
|
||||
self.last_results.extend(exclude_targets)
|
||||
last_results.append(Device(mac.upper(), name, ipv4, now))
|
||||
|
||||
self.last_results = last_results
|
||||
|
||||
_LOGGER.info("nmap scan successful")
|
||||
return True
|
||||
|
|
|
@ -33,7 +33,7 @@ def setup_scanner(hass, config, see):
|
|||
'Unable to parse payload as JSON: %s', payload)
|
||||
return
|
||||
|
||||
if data.get('_type') != 'location':
|
||||
if not isinstance(data, dict) or data.get('_type') != 'location':
|
||||
return
|
||||
|
||||
parts = topic.split('/')
|
||||
|
|
119
homeassistant/components/device_tracker/snmp.py
Normal file
119
homeassistant/components/device_tracker/snmp.py
Normal file
|
@ -0,0 +1,119 @@
|
|||
"""
|
||||
homeassistant.components.device_tracker.snmp
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Device tracker platform that supports fetching WiFi associations
|
||||
through SNMP.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_tracker.snmp.html
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
import threading
|
||||
import binascii
|
||||
|
||||
from homeassistant.const import CONF_HOST
|
||||
from homeassistant.helpers import validate_config
|
||||
from homeassistant.util import Throttle
|
||||
from homeassistant.components.device_tracker import DOMAIN
|
||||
|
||||
# Return cached results if last scan was less then this time ago
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
REQUIREMENTS = ['pysnmp==4.2.5']
|
||||
|
||||
CONF_COMMUNITY = "community"
|
||||
CONF_BASEOID = "baseoid"
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def get_scanner(hass, config):
|
||||
""" Validates config and returns an snmp scanner """
|
||||
if not validate_config(config,
|
||||
{DOMAIN: [CONF_HOST, CONF_COMMUNITY, CONF_BASEOID]},
|
||||
_LOGGER):
|
||||
return None
|
||||
|
||||
scanner = SnmpScanner(config[DOMAIN])
|
||||
|
||||
return scanner if scanner.success_init else None
|
||||
|
||||
|
||||
class SnmpScanner(object):
|
||||
"""
|
||||
This class queries any SNMP capable Acces Point for connected devices.
|
||||
"""
|
||||
def __init__(self, config):
|
||||
self.host = config[CONF_HOST]
|
||||
self.community = config[CONF_COMMUNITY]
|
||||
self.baseoid = config[CONF_BASEOID]
|
||||
|
||||
self.lock = threading.Lock()
|
||||
|
||||
self.last_results = []
|
||||
|
||||
# Test the router is accessible
|
||||
data = self.get_snmp_data()
|
||||
self.success_init = data is not None
|
||||
|
||||
def scan_devices(self):
|
||||
"""
|
||||
Scans for new devices and return a list containing found device IDs.
|
||||
"""
|
||||
|
||||
self._update_info()
|
||||
return [client['mac'] for client in self.last_results]
|
||||
|
||||
# Supressing no-self-use warning
|
||||
# pylint: disable=R0201
|
||||
def get_device_name(self, device):
|
||||
""" Returns the name of the given device or None if we don't know. """
|
||||
# We have no names
|
||||
return None
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
||||
def _update_info(self):
|
||||
"""
|
||||
Ensures the information from the WAP is up to date.
|
||||
Returns boolean if scanning successful.
|
||||
"""
|
||||
if not self.success_init:
|
||||
return False
|
||||
|
||||
with self.lock:
|
||||
data = self.get_snmp_data()
|
||||
if not data:
|
||||
return False
|
||||
|
||||
self.last_results = data
|
||||
return True
|
||||
|
||||
def get_snmp_data(self):
|
||||
""" Fetch mac addresses from WAP via SNMP. """
|
||||
from pysnmp.entity.rfc3413.oneliner import cmdgen
|
||||
|
||||
devices = []
|
||||
|
||||
snmp = cmdgen.CommandGenerator()
|
||||
errindication, errstatus, errindex, restable = snmp.nextCmd(
|
||||
cmdgen.CommunityData(self.community),
|
||||
cmdgen.UdpTransportTarget((self.host, 161)),
|
||||
cmdgen.MibVariable(self.baseoid)
|
||||
)
|
||||
|
||||
if errindication:
|
||||
_LOGGER.error("SNMPLIB error: %s", errindication)
|
||||
return
|
||||
if errstatus:
|
||||
_LOGGER.error('SNMP error: %s at %s', errstatus.prettyPrint(),
|
||||
errindex and restable[-1][int(errindex)-1]
|
||||
or '?')
|
||||
return
|
||||
|
||||
for resrow in restable:
|
||||
for _, val in resrow:
|
||||
mac = binascii.hexlify(val.asOctets()).decode('utf-8')
|
||||
mac = ':'.join([mac[i:i+2] for i in range(0, len(mac), 2)])
|
||||
devices.append({'mac': mac})
|
||||
return devices
|
|
@ -19,7 +19,7 @@ from homeassistant.const import (
|
|||
|
||||
DOMAIN = "discovery"
|
||||
DEPENDENCIES = []
|
||||
REQUIREMENTS = ['netdisco==0.4.1']
|
||||
REQUIREMENTS = ['netdisco==0.4.2']
|
||||
|
||||
SCAN_INTERVAL = 300 # seconds
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ import logging
|
|||
from . import version
|
||||
import homeassistant.util as util
|
||||
from homeassistant.const import URL_ROOT, HTTP_OK
|
||||
from homeassistant.config import get_default_config_dir
|
||||
|
||||
DOMAIN = 'frontend'
|
||||
DEPENDENCIES = ['api']
|
||||
|
@ -19,7 +20,6 @@ INDEX_PATH = os.path.join(os.path.dirname(__file__), 'index.html.template')
|
|||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
FRONTEND_URLS = [
|
||||
URL_ROOT, '/logbook', '/history', '/map', '/devService', '/devState',
|
||||
'/devEvent']
|
||||
|
@ -44,6 +44,9 @@ def setup(hass, config):
|
|||
hass.http.register_path(
|
||||
'HEAD', re.compile(r'/static/(?P<file>[a-zA-Z\._\-0-9/]+)'),
|
||||
_handle_get_static, False)
|
||||
hass.http.register_path(
|
||||
'GET', re.compile(r'/local/(?P<file>[a-zA-Z\._\-0-9/]+)'),
|
||||
_handle_get_local, False)
|
||||
|
||||
return True
|
||||
|
||||
|
@ -84,3 +87,16 @@ def _handle_get_static(handler, path_match, data):
|
|||
path = os.path.join(os.path.dirname(__file__), 'www_static', req_file)
|
||||
|
||||
handler.write_file(path)
|
||||
|
||||
|
||||
def _handle_get_local(handler, path_match, data):
|
||||
"""
|
||||
Returns a static file from the hass.config.path/www for the frontend.
|
||||
"""
|
||||
req_file = util.sanitize_path(path_match.group('file'))
|
||||
|
||||
path = os.path.join(get_default_config_dir(), 'www', req_file)
|
||||
if not os.path.isfile(path):
|
||||
return False
|
||||
|
||||
handler.write_file(path)
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
""" DO NOT MODIFY. Auto-generated by build_frontend script """
|
||||
VERSION = "3a3ed81f9d66bf24e17f1d02b8403335"
|
||||
VERSION = "c4722afa376379bc4457d54bb9a38cee"
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -1 +1 @@
|
|||
Subproject commit 6989009b2d59e39fd39b3025ff5899877f618bd3
|
||||
Subproject commit 3d6792691a3d6beae5d446a6fbeb83c9025d040d
|
|
@ -12,7 +12,8 @@ from homeassistant.helpers.entity import Entity
|
|||
import homeassistant.util as util
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, STATE_ON, STATE_OFF,
|
||||
STATE_HOME, STATE_NOT_HOME, STATE_UNKNOWN)
|
||||
STATE_HOME, STATE_NOT_HOME, STATE_OPEN, STATE_CLOSED,
|
||||
STATE_UNKNOWN)
|
||||
|
||||
DOMAIN = "group"
|
||||
DEPENDENCIES = []
|
||||
|
@ -22,7 +23,8 @@ ENTITY_ID_FORMAT = DOMAIN + ".{}"
|
|||
ATTR_AUTO = "auto"
|
||||
|
||||
# List of ON/OFF state tuples for groupable states
|
||||
_GROUP_TYPES = [(STATE_ON, STATE_OFF), (STATE_HOME, STATE_NOT_HOME)]
|
||||
_GROUP_TYPES = [(STATE_ON, STATE_OFF), (STATE_HOME, STATE_NOT_HOME),
|
||||
(STATE_OPEN, STATE_CLOSED)]
|
||||
|
||||
|
||||
def _get_group_on_off(state):
|
||||
|
|
|
@ -232,7 +232,12 @@ class RequestHandler(SimpleHTTPRequestHandler):
|
|||
|
||||
def log_message(self, fmt, *arguments):
|
||||
""" Redirect built-in log to HA logging """
|
||||
_LOGGER.info(fmt, *arguments)
|
||||
if self.server.no_password_set:
|
||||
_LOGGER.info(fmt, *arguments)
|
||||
else:
|
||||
_LOGGER.info(
|
||||
fmt, *(arg.replace(self.server.api_password, '*******')
|
||||
if isinstance(arg, str) else arg for arg in arguments))
|
||||
|
||||
def _handle_request(self, method): # pylint: disable=too-many-branches
|
||||
""" Does some common checks and calls appropriate method. """
|
||||
|
|
|
@ -52,14 +52,14 @@ import logging
|
|||
import os
|
||||
import csv
|
||||
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.entity import ToggleEntity
|
||||
|
||||
import homeassistant.util as util
|
||||
import homeassistant.util.color as color_util
|
||||
from homeassistant.components import group, discovery, wink, isy994
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.const import (
|
||||
STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID)
|
||||
from homeassistant.components import group, discovery, wink, isy994
|
||||
from homeassistant.helpers.entity import ToggleEntity
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
import homeassistant.util as util
|
||||
import homeassistant.util.color as color_util
|
||||
|
||||
|
||||
DOMAIN = "light"
|
||||
|
@ -246,6 +246,7 @@ def setup(hass, config):
|
|||
rgb_color = dat.get(ATTR_RGB_COLOR)
|
||||
|
||||
if len(rgb_color) == 3:
|
||||
params[ATTR_RGB_COLOR] = [int(val) for val in rgb_color]
|
||||
params[ATTR_XY_COLOR] = \
|
||||
color_util.color_RGB_to_xy(int(rgb_color[0]),
|
||||
int(rgb_color[1]),
|
||||
|
@ -275,11 +276,13 @@ def setup(hass, config):
|
|||
light.update_ha_state(True)
|
||||
|
||||
# Listen for light on and light off service calls
|
||||
hass.services.register(DOMAIN, SERVICE_TURN_ON,
|
||||
handle_light_service)
|
||||
descriptions = load_yaml_config_file(
|
||||
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
||||
hass.services.register(DOMAIN, SERVICE_TURN_ON, handle_light_service,
|
||||
descriptions.get(SERVICE_TURN_ON))
|
||||
|
||||
hass.services.register(DOMAIN, SERVICE_TURN_OFF,
|
||||
handle_light_service)
|
||||
hass.services.register(DOMAIN, SERVICE_TURN_OFF, handle_light_service,
|
||||
descriptions.get(SERVICE_TURN_OFF))
|
||||
|
||||
return True
|
||||
|
||||
|
|
76
homeassistant/components/light/blinksticklight.py
Normal file
76
homeassistant/components/light/blinksticklight.py
Normal file
|
@ -0,0 +1,76 @@
|
|||
"""
|
||||
homeassistant.components.light.blinksticklight
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Support for Blinkstick lights.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/light.blinksticklight.html
|
||||
"""
|
||||
import logging
|
||||
|
||||
from blinkstick import blinkstick
|
||||
|
||||
from homeassistant.components.light import (Light, ATTR_RGB_COLOR)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
REQUIREMENTS = ["blinkstick==1.1.7"]
|
||||
DEPENDENCIES = []
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
""" Add device specified by serial number. """
|
||||
stick = blinkstick.find_by_serial(config['serial'])
|
||||
|
||||
add_devices_callback([BlinkStickLight(stick, config['name'])])
|
||||
|
||||
|
||||
class BlinkStickLight(Light):
|
||||
""" Represents a BlinkStick light. """
|
||||
|
||||
def __init__(self, stick, name):
|
||||
self._stick = stick
|
||||
self._name = name
|
||||
self._serial = stick.get_serial()
|
||||
self._rgb_color = stick.get_color()
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
""" Polling needed. """
|
||||
return True
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" The name of the light. """
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def rgb_color(self):
|
||||
""" Read back the color of the light. """
|
||||
return self._rgb_color
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
""" Check whether any of the LEDs colors are non-zero. """
|
||||
return sum(self._rgb_color) > 0
|
||||
|
||||
def update(self):
|
||||
""" Read back the device state """
|
||||
self._rgb_color = self._stick.get_color()
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
""" Turn the device on. """
|
||||
if ATTR_RGB_COLOR in kwargs:
|
||||
self._rgb_color = kwargs[ATTR_RGB_COLOR]
|
||||
else:
|
||||
self._rgb_color = [255, 255, 255]
|
||||
|
||||
self._stick.set_color(red=self._rgb_color[0],
|
||||
green=self._rgb_color[1],
|
||||
blue=self._rgb_color[2])
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
""" Turn the device off """
|
||||
self._stick.turn_off()
|
|
@ -19,11 +19,15 @@ configuration.yaml file.
|
|||
|
||||
light:
|
||||
platform: limitlessled
|
||||
host: 192.168.1.10
|
||||
group_1_name: Living Room
|
||||
group_2_name: Bedroom
|
||||
group_3_name: Office
|
||||
group_4_name: Kitchen
|
||||
bridges:
|
||||
- host: 192.168.1.10
|
||||
group_1_name: Living Room
|
||||
group_2_name: Bedroom
|
||||
group_3_name: Office
|
||||
group_3_type: white
|
||||
group_4_name: Kitchen
|
||||
- host: 192.168.1.11
|
||||
group_2_name: Basement
|
||||
"""
|
||||
import logging
|
||||
|
||||
|
@ -33,19 +37,30 @@ from homeassistant.components.light import (Light, ATTR_BRIGHTNESS,
|
|||
from homeassistant.util.color import color_RGB_to_xy
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
REQUIREMENTS = ['ledcontroller==1.0.7']
|
||||
REQUIREMENTS = ['ledcontroller==1.1.0']
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
""" Gets the LimitlessLED lights. """
|
||||
import ledcontroller
|
||||
|
||||
led = ledcontroller.LedController(config['host'])
|
||||
# Handle old configuration format:
|
||||
bridges = config.get('bridges', [config])
|
||||
|
||||
for bridge_id, bridge in enumerate(bridges):
|
||||
bridge['id'] = bridge_id
|
||||
|
||||
pool = ledcontroller.LedControllerPool([x['host'] for x in bridges])
|
||||
|
||||
lights = []
|
||||
for i in range(1, 5):
|
||||
if 'group_%d_name' % (i) in config:
|
||||
lights.append(LimitlessLED(led, i, config['group_%d_name' % (i)]))
|
||||
for bridge in bridges:
|
||||
for i in range(1, 5):
|
||||
name_key = 'group_%d_name' % i
|
||||
if name_key in bridge:
|
||||
group_type = bridge.get('group_%d_type' % i, 'rgbw')
|
||||
lights.append(LimitlessLED.factory(pool, bridge['id'], i,
|
||||
bridge[name_key],
|
||||
group_type))
|
||||
|
||||
add_devices_callback(lights)
|
||||
|
||||
|
@ -53,15 +68,57 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
|||
class LimitlessLED(Light):
|
||||
""" Represents a LimitlessLED light """
|
||||
|
||||
def __init__(self, led, group, name):
|
||||
self.led = led
|
||||
@staticmethod
|
||||
def factory(pool, controller_id, group, name, group_type):
|
||||
''' Construct a Limitless LED of the appropriate type '''
|
||||
if group_type == 'white':
|
||||
return WhiteLimitlessLED(pool, controller_id, group, name)
|
||||
elif group_type == 'rgbw':
|
||||
return RGBWLimitlessLED(pool, controller_id, group, name)
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def __init__(self, pool, controller_id, group, name, group_type):
|
||||
self.pool = pool
|
||||
self.controller_id = controller_id
|
||||
self.group = group
|
||||
|
||||
self.pool.execute(self.controller_id, "set_group_type", self.group,
|
||||
group_type)
|
||||
|
||||
# LimitlessLEDs don't report state, we have track it ourselves.
|
||||
self.led.off(self.group)
|
||||
self.pool.execute(self.controller_id, "off", self.group)
|
||||
|
||||
self._name = name or DEVICE_DEFAULT_NAME
|
||||
self._state = False
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
""" No polling needed. """
|
||||
return False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" Returns the name of the device if any. """
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
""" True if device is on. """
|
||||
return self._state
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
""" Turn the device off. """
|
||||
self._state = False
|
||||
self.pool.execute(self.controller_id, "off", self.group)
|
||||
self.update_ha_state()
|
||||
|
||||
|
||||
class RGBWLimitlessLED(LimitlessLED):
|
||||
""" Represents a RGBW LimitlessLED light """
|
||||
|
||||
def __init__(self, pool, controller_id, group, name):
|
||||
super().__init__(pool, controller_id, group, name, 'rgbw')
|
||||
|
||||
self._brightness = 100
|
||||
self._xy_color = color_RGB_to_xy(255, 255, 255)
|
||||
|
||||
|
@ -87,16 +144,6 @@ class LimitlessLED(Light):
|
|||
((0xE6, 0xE6, 0xFA), 'lavendar'),
|
||||
]]
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
""" No polling needed for a demo light. """
|
||||
return False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" Returns the name of the device if any. """
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def brightness(self):
|
||||
return self._brightness
|
||||
|
@ -117,11 +164,6 @@ class LimitlessLED(Light):
|
|||
# First candidate in the sorted list is closest to desired color:
|
||||
return sorted(candidates)[0][1]
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
""" True if device is on. """
|
||||
return self._state
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
""" Turn the device on. """
|
||||
self._state = True
|
||||
|
@ -132,12 +174,21 @@ class LimitlessLED(Light):
|
|||
if ATTR_XY_COLOR in kwargs:
|
||||
self._xy_color = kwargs[ATTR_XY_COLOR]
|
||||
|
||||
self.led.set_color(self._xy_to_led_color(self._xy_color), self.group)
|
||||
self.led.set_brightness(self._brightness / 255.0, self.group)
|
||||
self.pool.execute(self.controller_id, "set_color",
|
||||
self._xy_to_led_color(self._xy_color), self.group)
|
||||
self.pool.execute(self.controller_id, "set_brightness",
|
||||
self._brightness / 255.0, self.group)
|
||||
self.update_ha_state()
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
""" Turn the device off. """
|
||||
self._state = False
|
||||
self.led.off(self.group)
|
||||
|
||||
class WhiteLimitlessLED(LimitlessLED):
|
||||
""" Represents a White LimitlessLED light """
|
||||
|
||||
def __init__(self, pool, controller_id, group, name):
|
||||
super().__init__(pool, controller_id, group, name, 'white')
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
""" Turn the device on. """
|
||||
self._state = True
|
||||
self.pool.execute(self.controller_id, "on", self.group)
|
||||
self.update_ha_state()
|
||||
|
|
112
homeassistant/components/light/rfxtrx.py
Normal file
112
homeassistant/components/light/rfxtrx.py
Normal file
|
@ -0,0 +1,112 @@
|
|||
"""
|
||||
homeassistant.components.light.rfxtrx
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Support for RFXtrx lights.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/light.rfxtrx.html
|
||||
"""
|
||||
import logging
|
||||
import homeassistant.components.rfxtrx as rfxtrx
|
||||
import RFXtrx as rfxtrxmod
|
||||
|
||||
from homeassistant.components.light import Light
|
||||
from homeassistant.util import slugify
|
||||
|
||||
DEPENDENCIES = ['rfxtrx']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
""" Setup the RFXtrx platform. """
|
||||
lights = []
|
||||
devices = config.get('devices', None)
|
||||
if devices:
|
||||
for entity_id, entity_info in devices.items():
|
||||
if entity_id not in rfxtrx.RFX_DEVICES:
|
||||
_LOGGER.info("Add %s rfxtrx.light", entity_info['name'])
|
||||
rfxobject = rfxtrx.get_rfx_object(entity_info['packetid'])
|
||||
new_light = RfxtrxLight(entity_info['name'], rfxobject, False)
|
||||
rfxtrx.RFX_DEVICES[entity_id] = new_light
|
||||
lights.append(new_light)
|
||||
|
||||
add_devices_callback(lights)
|
||||
|
||||
def light_update(event):
|
||||
""" Callback for light updates from the RFXtrx gateway. """
|
||||
if not isinstance(event.device, rfxtrxmod.LightingDevice):
|
||||
return
|
||||
|
||||
# Add entity if not exist and the automatic_add is True
|
||||
entity_id = slugify(event.device.id_string.lower())
|
||||
if entity_id not in rfxtrx.RFX_DEVICES:
|
||||
automatic_add = config.get('automatic_add', False)
|
||||
if not automatic_add:
|
||||
return
|
||||
|
||||
_LOGGER.info(
|
||||
"Automatic add %s rfxtrx.light (Class: %s Sub: %s)",
|
||||
entity_id,
|
||||
event.device.__class__.__name__,
|
||||
event.device.subtype
|
||||
)
|
||||
pkt_id = "".join("{0:02x}".format(x) for x in event.data)
|
||||
entity_name = "%s : %s" % (entity_id, pkt_id)
|
||||
new_light = RfxtrxLight(entity_name, event, False)
|
||||
rfxtrx.RFX_DEVICES[entity_id] = new_light
|
||||
add_devices_callback([new_light])
|
||||
|
||||
# Check if entity exists or previously added automatically
|
||||
if entity_id in rfxtrx.RFX_DEVICES:
|
||||
if event.values['Command'] == 'On'\
|
||||
or event.values['Command'] == 'Off':
|
||||
if event.values['Command'] == 'On':
|
||||
rfxtrx.RFX_DEVICES[entity_id].turn_on()
|
||||
else:
|
||||
rfxtrx.RFX_DEVICES[entity_id].turn_off()
|
||||
|
||||
# Subscribe to main rfxtrx events
|
||||
if light_update not in rfxtrx.RECEIVED_EVT_SUBSCRIBERS:
|
||||
rfxtrx.RECEIVED_EVT_SUBSCRIBERS.append(light_update)
|
||||
|
||||
|
||||
class RfxtrxLight(Light):
|
||||
""" Provides a RFXtrx light. """
|
||||
def __init__(self, name, event, state):
|
||||
self._name = name
|
||||
self._event = event
|
||||
self._state = state
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
""" No polling needed for a light. """
|
||||
return False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" Returns the name of the light if any. """
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
""" True if light is on. """
|
||||
return self._state
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
""" Turn the light on. """
|
||||
|
||||
if hasattr(self, '_event') and self._event:
|
||||
self._event.device.send_on(rfxtrx.RFXOBJECT.transport)
|
||||
|
||||
self._state = True
|
||||
self.update_ha_state()
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
""" Turn the light off. """
|
||||
|
||||
if hasattr(self, '_event') and self._event:
|
||||
self._event.device.send_off(rfxtrx.RFXOBJECT.transport)
|
||||
|
||||
self._state = False
|
||||
self.update_ha_state()
|
52
homeassistant/components/light/services.yaml
Normal file
52
homeassistant/components/light/services.yaml
Normal file
|
@ -0,0 +1,52 @@
|
|||
# Describes the format for available light services
|
||||
|
||||
turn_on:
|
||||
description: Turn a light on
|
||||
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of entities to turn on
|
||||
example: 'light.kitchen'
|
||||
|
||||
transition:
|
||||
description: Duration in seconds it takes to get to next state
|
||||
example: 60
|
||||
|
||||
rgb_color:
|
||||
description: Color for the light in RGB-format
|
||||
example: '[255, 100, 100]'
|
||||
|
||||
xy_color:
|
||||
description: Color for the light in XY-format
|
||||
example: '[0.52, 0.43]'
|
||||
|
||||
brightness:
|
||||
description: Number between 0..255 indicating brightness
|
||||
example: 120
|
||||
|
||||
profile:
|
||||
description: Name of a light profile to use
|
||||
example: relax
|
||||
|
||||
flash:
|
||||
description: If the light should flash
|
||||
values:
|
||||
- short
|
||||
- long
|
||||
|
||||
effect:
|
||||
description: Light effect
|
||||
values:
|
||||
- colorloop
|
||||
|
||||
turn_off:
|
||||
description: Turn a light off
|
||||
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of entities to turn off
|
||||
example: 'light.kitchen'
|
||||
|
||||
transition:
|
||||
description: Duration in seconds it takes to get to next state
|
||||
example: 60
|
|
@ -6,10 +6,11 @@ Support for Tellstick lights.
|
|||
import logging
|
||||
# pylint: disable=no-name-in-module, import-error
|
||||
from homeassistant.components.light import Light, ATTR_BRIGHTNESS
|
||||
from homeassistant.const import ATTR_FRIENDLY_NAME
|
||||
from homeassistant.const import (EVENT_HOMEASSISTANT_STOP,
|
||||
ATTR_FRIENDLY_NAME)
|
||||
import tellcore.constants as tellcore_constants
|
||||
from tellcore.library import DirectCallbackDispatcher
|
||||
REQUIREMENTS = ['tellcore-py==1.0.4']
|
||||
REQUIREMENTS = ['tellcore-py==1.1.2']
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
|
@ -23,12 +24,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
|||
"Failed to import tellcore")
|
||||
return []
|
||||
|
||||
# pylint: disable=no-member
|
||||
if telldus.TelldusCore.callback_dispatcher is None:
|
||||
dispatcher = DirectCallbackDispatcher()
|
||||
core = telldus.TelldusCore(callback_dispatcher=dispatcher)
|
||||
else:
|
||||
core = telldus.TelldusCore()
|
||||
core = telldus.TelldusCore(callback_dispatcher=DirectCallbackDispatcher())
|
||||
|
||||
switches_and_lights = core.devices()
|
||||
lights = []
|
||||
|
@ -41,9 +37,18 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
|||
""" Called from the TelldusCore library to update one device """
|
||||
for light_device in lights:
|
||||
if light_device.tellstick_device.id == id_:
|
||||
# Execute the update in another thread
|
||||
light_device.update_ha_state(True)
|
||||
break
|
||||
|
||||
core.register_device_event(_device_event_callback)
|
||||
callback_id = core.register_device_event(_device_event_callback)
|
||||
|
||||
def unload_telldus_lib(event):
|
||||
""" Un-register the callback bindings """
|
||||
if callback_id is not None:
|
||||
core.unregister_callback(callback_id)
|
||||
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, unload_telldus_lib)
|
||||
|
||||
add_devices_callback(lights)
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ import re
|
|||
|
||||
from homeassistant.core import State, DOMAIN as HA_DOMAIN
|
||||
from homeassistant.const import (
|
||||
EVENT_STATE_CHANGED, STATE_HOME, STATE_ON, STATE_OFF,
|
||||
EVENT_STATE_CHANGED, STATE_NOT_HOME, STATE_ON, STATE_OFF,
|
||||
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, HTTP_BAD_REQUEST)
|
||||
from homeassistant import util
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
@ -162,10 +162,12 @@ def humanify(events):
|
|||
|
||||
to_state = State.from_dict(event.data.get('new_state'))
|
||||
|
||||
# if last_changed == last_updated only attributes have changed
|
||||
# we do not report on that yet.
|
||||
# if last_changed != last_updated only attributes have changed
|
||||
# we do not report on that yet. Also filter auto groups.
|
||||
if not to_state or \
|
||||
to_state.last_changed != to_state.last_updated:
|
||||
to_state.last_changed != to_state.last_updated or \
|
||||
to_state.domain == 'group' and \
|
||||
to_state.attributes.get('auto', False):
|
||||
continue
|
||||
|
||||
domain = to_state.domain
|
||||
|
@ -218,10 +220,13 @@ def humanify(events):
|
|||
def _entry_message_from_state(domain, state):
|
||||
""" Convert a state to a message for the logbook. """
|
||||
# We pass domain in so we don't have to split entity_id again
|
||||
# pylint: disable=too-many-return-statements
|
||||
|
||||
if domain == 'device_tracker':
|
||||
return '{} home'.format(
|
||||
'arrived' if state.state == STATE_HOME else 'left')
|
||||
if state.state == STATE_NOT_HOME:
|
||||
return 'is away'
|
||||
else:
|
||||
return 'is at {}'.format(state.state)
|
||||
|
||||
elif domain == 'sun':
|
||||
if state.state == sun.STATE_ABOVE_HORIZON:
|
||||
|
|
|
@ -5,8 +5,10 @@ homeassistant.components.media_player
|
|||
Component to interface with various media players.
|
||||
"""
|
||||
import logging
|
||||
import os
|
||||
|
||||
from homeassistant.components import discovery
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.const import (
|
||||
|
@ -29,6 +31,7 @@ DISCOVERY_PLATFORMS = {
|
|||
}
|
||||
|
||||
SERVICE_YOUTUBE_VIDEO = 'play_youtube_video'
|
||||
SERVICE_PLAY_MEDIA = 'play_media'
|
||||
|
||||
ATTR_MEDIA_VOLUME_LEVEL = 'volume_level'
|
||||
ATTR_MEDIA_VOLUME_MUTED = 'is_volume_muted'
|
||||
|
@ -44,6 +47,8 @@ ATTR_MEDIA_TRACK = 'media_track'
|
|||
ATTR_MEDIA_SERIES_TITLE = 'media_series_title'
|
||||
ATTR_MEDIA_SEASON = 'media_season'
|
||||
ATTR_MEDIA_EPISODE = 'media_episode'
|
||||
ATTR_MEDIA_CHANNEL = 'media_channel'
|
||||
ATTR_MEDIA_PLAYLIST = 'media_playlist'
|
||||
ATTR_APP_ID = 'app_id'
|
||||
ATTR_APP_NAME = 'app_name'
|
||||
ATTR_SUPPORTED_MEDIA_COMMANDS = 'supported_media_commands'
|
||||
|
@ -51,6 +56,9 @@ ATTR_SUPPORTED_MEDIA_COMMANDS = 'supported_media_commands'
|
|||
MEDIA_TYPE_MUSIC = 'music'
|
||||
MEDIA_TYPE_TVSHOW = 'tvshow'
|
||||
MEDIA_TYPE_VIDEO = 'movie'
|
||||
MEDIA_TYPE_EPISODE = 'episode'
|
||||
MEDIA_TYPE_CHANNEL = 'channel'
|
||||
MEDIA_TYPE_PLAYLIST = 'playlist'
|
||||
|
||||
SUPPORT_PAUSE = 1
|
||||
SUPPORT_SEEK = 2
|
||||
|
@ -61,6 +69,7 @@ SUPPORT_NEXT_TRACK = 32
|
|||
SUPPORT_YOUTUBE = 64
|
||||
SUPPORT_TURN_ON = 128
|
||||
SUPPORT_TURN_OFF = 256
|
||||
SUPPORT_PLAY_MEDIA = 512
|
||||
|
||||
YOUTUBE_COVER_URL_FORMAT = 'https://img.youtube.com/vi/{}/1.jpg'
|
||||
|
||||
|
@ -74,6 +83,7 @@ SERVICE_TO_METHOD = {
|
|||
SERVICE_MEDIA_PAUSE: 'media_pause',
|
||||
SERVICE_MEDIA_NEXT_TRACK: 'media_next_track',
|
||||
SERVICE_MEDIA_PREVIOUS_TRACK: 'media_previous_track',
|
||||
SERVICE_PLAY_MEDIA: 'play_media',
|
||||
}
|
||||
|
||||
ATTR_TO_PROPERTY = [
|
||||
|
@ -90,6 +100,8 @@ ATTR_TO_PROPERTY = [
|
|||
ATTR_MEDIA_SERIES_TITLE,
|
||||
ATTR_MEDIA_SEASON,
|
||||
ATTR_MEDIA_EPISODE,
|
||||
ATTR_MEDIA_CHANNEL,
|
||||
ATTR_MEDIA_PLAYLIST,
|
||||
ATTR_APP_ID,
|
||||
ATTR_APP_NAME,
|
||||
ATTR_SUPPORTED_MEDIA_COMMANDS,
|
||||
|
@ -178,6 +190,16 @@ def media_previous_track(hass, entity_id=None):
|
|||
hass.services.call(DOMAIN, SERVICE_MEDIA_PREVIOUS_TRACK, data)
|
||||
|
||||
|
||||
def play_media(hass, media_type, media_id, entity_id=None):
|
||||
""" Send the media player the command for playing media. """
|
||||
data = {"media_type": media_type, "media_id": media_id}
|
||||
|
||||
if entity_id:
|
||||
data[ATTR_ENTITY_ID] = entity_id
|
||||
|
||||
hass.services.call(DOMAIN, SERVICE_PLAY_MEDIA, data)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
""" Track states and offer events for media_players. """
|
||||
component = EntityComponent(
|
||||
|
@ -186,6 +208,9 @@ def setup(hass, config):
|
|||
|
||||
component.setup(config)
|
||||
|
||||
descriptions = load_yaml_config_file(
|
||||
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
||||
|
||||
def media_player_service_handler(service):
|
||||
""" Maps services to methods on MediaPlayerDevice. """
|
||||
target_players = component.extract_from_service(service)
|
||||
|
@ -199,7 +224,8 @@ def setup(hass, config):
|
|||
player.update_ha_state(True)
|
||||
|
||||
for service in SERVICE_TO_METHOD:
|
||||
hass.services.register(DOMAIN, service, media_player_service_handler)
|
||||
hass.services.register(DOMAIN, service, media_player_service_handler,
|
||||
descriptions.get(service))
|
||||
|
||||
def volume_set_service(service):
|
||||
""" Set specified volume on the media player. """
|
||||
|
@ -216,7 +242,8 @@ def setup(hass, config):
|
|||
if player.should_poll:
|
||||
player.update_ha_state(True)
|
||||
|
||||
hass.services.register(DOMAIN, SERVICE_VOLUME_SET, volume_set_service)
|
||||
hass.services.register(DOMAIN, SERVICE_VOLUME_SET, volume_set_service,
|
||||
descriptions.get(SERVICE_VOLUME_SET))
|
||||
|
||||
def volume_mute_service(service):
|
||||
""" Mute (true) or unmute (false) the media player. """
|
||||
|
@ -233,7 +260,8 @@ def setup(hass, config):
|
|||
if player.should_poll:
|
||||
player.update_ha_state(True)
|
||||
|
||||
hass.services.register(DOMAIN, SERVICE_VOLUME_MUTE, volume_mute_service)
|
||||
hass.services.register(DOMAIN, SERVICE_VOLUME_MUTE, volume_mute_service,
|
||||
descriptions.get(SERVICE_VOLUME_MUTE))
|
||||
|
||||
def media_seek_service(service):
|
||||
""" Seek to a position. """
|
||||
|
@ -250,7 +278,8 @@ def setup(hass, config):
|
|||
if player.should_poll:
|
||||
player.update_ha_state(True)
|
||||
|
||||
hass.services.register(DOMAIN, SERVICE_MEDIA_SEEK, media_seek_service)
|
||||
hass.services.register(DOMAIN, SERVICE_MEDIA_SEEK, media_seek_service,
|
||||
descriptions.get(SERVICE_MEDIA_SEEK))
|
||||
|
||||
def play_youtube_video_service(service, media_id=None):
|
||||
""" Plays specified media_id on the media player. """
|
||||
|
@ -266,16 +295,40 @@ def setup(hass, config):
|
|||
if player.should_poll:
|
||||
player.update_ha_state(True)
|
||||
|
||||
def play_media_service(service):
|
||||
""" Plays specified media_id on the media player. """
|
||||
media_type = service.data.get('media_type')
|
||||
media_id = service.data.get('media_id')
|
||||
|
||||
if media_type is None:
|
||||
return
|
||||
|
||||
if media_id is None:
|
||||
return
|
||||
|
||||
for player in component.extract_from_service(service):
|
||||
player.play_media(media_type, media_id)
|
||||
|
||||
if player.should_poll:
|
||||
player.update_ha_state(True)
|
||||
|
||||
hass.services.register(
|
||||
DOMAIN, "start_fireplace",
|
||||
lambda service: play_youtube_video_service(service, "eyU3bRy2x44"))
|
||||
lambda service: play_youtube_video_service(service, "eyU3bRy2x44"),
|
||||
descriptions.get('start_fireplace'))
|
||||
|
||||
hass.services.register(
|
||||
DOMAIN, "start_epic_sax",
|
||||
lambda service: play_youtube_video_service(service, "kxopViU98Xo"))
|
||||
lambda service: play_youtube_video_service(service, "kxopViU98Xo"),
|
||||
descriptions.get('start_epic_sax'))
|
||||
|
||||
hass.services.register(
|
||||
DOMAIN, SERVICE_YOUTUBE_VIDEO, play_youtube_video_service)
|
||||
DOMAIN, SERVICE_YOUTUBE_VIDEO, play_youtube_video_service,
|
||||
descriptions.get(SERVICE_YOUTUBE_VIDEO))
|
||||
|
||||
hass.services.register(
|
||||
DOMAIN, SERVICE_PLAY_MEDIA, play_media_service,
|
||||
descriptions.get(SERVICE_PLAY_MEDIA))
|
||||
|
||||
return True
|
||||
|
||||
|
@ -361,6 +414,16 @@ class MediaPlayerDevice(Entity):
|
|||
""" Episode of current playing media. (TV Show only) """
|
||||
return None
|
||||
|
||||
@property
|
||||
def media_channel(self):
|
||||
""" Channel currently playing. """
|
||||
return None
|
||||
|
||||
@property
|
||||
def media_playlist(self):
|
||||
""" Title of Playlist currently playing. """
|
||||
return None
|
||||
|
||||
@property
|
||||
def app_id(self):
|
||||
""" ID of the current running app. """
|
||||
|
@ -421,6 +484,10 @@ class MediaPlayerDevice(Entity):
|
|||
""" Plays a YouTube media. """
|
||||
raise NotImplementedError()
|
||||
|
||||
def play_media(self, media_type, media_id):
|
||||
""" Plays a piece of media. """
|
||||
raise NotImplementedError()
|
||||
|
||||
# No need to overwrite these.
|
||||
@property
|
||||
def support_pause(self):
|
||||
|
@ -457,6 +524,11 @@ class MediaPlayerDevice(Entity):
|
|||
""" Boolean if YouTube is supported. """
|
||||
return bool(self.supported_media_commands & SUPPORT_YOUTUBE)
|
||||
|
||||
@property
|
||||
def support_play_media(self):
|
||||
""" Boolean if play media command supported. """
|
||||
return bool(self.supported_media_commands & SUPPORT_PLAY_MEDIA)
|
||||
|
||||
def volume_up(self):
|
||||
""" volume_up media player. """
|
||||
if self.volume_level < 1:
|
||||
|
|
|
@ -90,6 +90,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
class CastDevice(MediaPlayerDevice):
|
||||
""" Represents a Cast device on the network. """
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
# pylint: disable=too-many-public-methods
|
||||
|
||||
def __init__(self, host):
|
||||
|
|
233
homeassistant/components/media_player/firetv.py
Normal file
233
homeassistant/components/media_player/firetv.py
Normal file
|
@ -0,0 +1,233 @@
|
|||
"""
|
||||
homeassistant.components.media_player.firetv
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Provides control over an Amazon Fire TV (/stick) via
|
||||
python-firetv, a Python 2.x module with a helper script
|
||||
that exposes a HTTP server to fetch state and perform
|
||||
actions.
|
||||
|
||||
Steps to configure your Amazon Fire TV stick with Home Assistant:
|
||||
|
||||
1. Turn on ADB Debugging on your Amazon Fire TV:
|
||||
a. From the main (Launcher) screen, select Settings.
|
||||
b. Select System > Developer Options.
|
||||
c. Select ADB Debugging.
|
||||
2. Find Amazon Fire TV device IP:
|
||||
a. From the main (Launcher) screen, select Settings.
|
||||
b. Select System > About > Network.
|
||||
3. `pip install firetv[firetv-server]` into a Python 2.x environment
|
||||
4. `firetv-server -d <fire tv device IP>:5555`, background the process
|
||||
5. Configure Home Assistant as follows:
|
||||
|
||||
media_player:
|
||||
platform: firetv
|
||||
# optional: where firetv-server is running (default is 'localhost:5556')
|
||||
host: localhost:5556
|
||||
# optional: device id (default is 'default')
|
||||
device: livingroom-firetv
|
||||
# optional: friendly name (default is 'Amazon Fire TV')
|
||||
name: My Amazon Fire TV
|
||||
|
||||
Note that python-firetv has support for multiple Amazon Fire TV devices.
|
||||
If you have more than one configured, be sure to specify the device id used.
|
||||
Run `firetv-server -h` and/or view the source for complete capabilities.
|
||||
|
||||
Possible states are:
|
||||
- off (TV screen is dark)
|
||||
- standby (standard UI is active - not apps)
|
||||
- idle (screen saver is active)
|
||||
- play (video is playing)
|
||||
- pause (video is paused)
|
||||
- disconnected (can't communicate with device)
|
||||
"""
|
||||
|
||||
import logging
|
||||
import requests
|
||||
|
||||
from homeassistant.const import (
|
||||
STATE_PLAYING, STATE_PAUSED, STATE_IDLE, STATE_OFF,
|
||||
STATE_UNKNOWN, STATE_STANDBY)
|
||||
|
||||
from homeassistant.components.media_player import (
|
||||
MediaPlayerDevice,
|
||||
SUPPORT_PAUSE, SUPPORT_VOLUME_SET,
|
||||
SUPPORT_TURN_ON, SUPPORT_TURN_OFF,
|
||||
SUPPORT_PREVIOUS_TRACK, SUPPORT_NEXT_TRACK)
|
||||
|
||||
SUPPORT_FIRETV = SUPPORT_PAUSE | \
|
||||
SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PREVIOUS_TRACK | \
|
||||
SUPPORT_NEXT_TRACK | SUPPORT_VOLUME_SET
|
||||
|
||||
DOMAIN = 'firetv'
|
||||
DEVICE_LIST_URL = 'http://{0}/devices/list'
|
||||
DEVICE_STATE_URL = 'http://{0}/devices/state/{1}'
|
||||
DEVICE_ACTION_URL = 'http://{0}/devices/action/{1}/{2}'
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
""" Sets up the firetv platform. """
|
||||
host = config.get('host', 'localhost:5556')
|
||||
device_id = config.get('device', 'default')
|
||||
try:
|
||||
response = requests.get(DEVICE_LIST_URL.format(host)).json()
|
||||
if device_id in response['devices'].keys():
|
||||
add_devices([
|
||||
FireTVDevice(
|
||||
host,
|
||||
device_id,
|
||||
config.get('name', 'Amazon Fire TV')
|
||||
)
|
||||
])
|
||||
_LOGGER.info(
|
||||
'Device %s accessible and ready for control', device_id)
|
||||
else:
|
||||
_LOGGER.warn(
|
||||
'Device %s is not registered with firetv-server', device_id)
|
||||
except requests.exceptions.RequestException:
|
||||
_LOGGER.error('Could not connect to firetv-server at %s', host)
|
||||
|
||||
|
||||
class FireTV(object):
|
||||
""" firetv-server client.
|
||||
|
||||
Should a native Python 3 ADB module become available,
|
||||
python-firetv can support Python 3, it can be added
|
||||
as a dependency, and this class can be dispensed of.
|
||||
|
||||
For now, it acts as a client to the firetv-server
|
||||
HTTP server (which must be running via Python 2).
|
||||
"""
|
||||
|
||||
def __init__(self, host, device_id):
|
||||
self.host = host
|
||||
self.device_id = device_id
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
""" Get the device state.
|
||||
|
||||
An exception means UNKNOWN state.
|
||||
"""
|
||||
try:
|
||||
response = requests.get(
|
||||
DEVICE_STATE_URL.format(
|
||||
self.host,
|
||||
self.device_id
|
||||
)
|
||||
).json()
|
||||
return response.get('state', STATE_UNKNOWN)
|
||||
except requests.exceptions.RequestException:
|
||||
_LOGGER.error(
|
||||
'Could not retrieve device state for %s', self.device_id)
|
||||
return STATE_UNKNOWN
|
||||
|
||||
def action(self, action_id):
|
||||
""" Perform an action on the device.
|
||||
|
||||
There is no action acknowledgment, so exceptions
|
||||
result in a pass.
|
||||
"""
|
||||
try:
|
||||
requests.get(
|
||||
DEVICE_ACTION_URL.format(
|
||||
self.host,
|
||||
self.device_id,
|
||||
action_id
|
||||
)
|
||||
)
|
||||
except requests.exceptions.RequestException:
|
||||
_LOGGER.error(
|
||||
'Action request for %s was not accepted for device %s',
|
||||
action_id, self.device_id)
|
||||
|
||||
|
||||
class FireTVDevice(MediaPlayerDevice):
|
||||
""" Represents an Amazon Fire TV device on the network. """
|
||||
|
||||
def __init__(self, host, device, name):
|
||||
self._firetv = FireTV(host, device)
|
||||
self._name = name
|
||||
self._state = STATE_UNKNOWN
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" Get the device name. """
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
""" Device should be polled. """
|
||||
return True
|
||||
|
||||
@property
|
||||
def supported_media_commands(self):
|
||||
""" Flags of media commands that are supported. """
|
||||
return SUPPORT_FIRETV
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
""" State of the player. """
|
||||
return self._state
|
||||
|
||||
def update(self):
|
||||
""" Update device state. """
|
||||
self._state = {
|
||||
'idle': STATE_IDLE,
|
||||
'off': STATE_OFF,
|
||||
'play': STATE_PLAYING,
|
||||
'pause': STATE_PAUSED,
|
||||
'standby': STATE_STANDBY,
|
||||
'disconnected': STATE_UNKNOWN,
|
||||
}.get(self._firetv.state, STATE_UNKNOWN)
|
||||
|
||||
def turn_on(self):
|
||||
""" Turns on the device. """
|
||||
self._firetv.action('turn_on')
|
||||
|
||||
def turn_off(self):
|
||||
""" Turns off the device. """
|
||||
self._firetv.action('turn_off')
|
||||
|
||||
def media_play(self):
|
||||
""" Send play commmand. """
|
||||
self._firetv.action('media_play')
|
||||
|
||||
def media_pause(self):
|
||||
""" Send pause command. """
|
||||
self._firetv.action('media_pause')
|
||||
|
||||
def media_play_pause(self):
|
||||
""" Send play/pause command. """
|
||||
self._firetv.action('media_play_pause')
|
||||
|
||||
def volume_up(self):
|
||||
""" Send volume up command. """
|
||||
self._firetv.action('volume_up')
|
||||
|
||||
def volume_down(self):
|
||||
""" Send volume down command. """
|
||||
self._firetv.action('volume_down')
|
||||
|
||||
def media_previous_track(self):
|
||||
""" Send previous track command (results in rewind). """
|
||||
self._firetv.action('media_previous')
|
||||
|
||||
def media_next_track(self):
|
||||
""" Send next track command (results in fast-forward). """
|
||||
self._firetv.action('media_next')
|
||||
|
||||
def media_seek(self, position):
|
||||
raise NotImplementedError()
|
||||
|
||||
def mute_volume(self, mute):
|
||||
raise NotImplementedError()
|
||||
|
||||
def play_youtube(self, media_id):
|
||||
raise NotImplementedError()
|
||||
|
||||
def set_volume_level(self, volume):
|
||||
raise NotImplementedError()
|
|
@ -35,9 +35,10 @@ URL of your running version of iTunes-API. Example: http://192.168.1.50:8181
|
|||
import logging
|
||||
|
||||
from homeassistant.components.media_player import (
|
||||
MediaPlayerDevice, MEDIA_TYPE_MUSIC, SUPPORT_PAUSE, SUPPORT_SEEK,
|
||||
SUPPORT_VOLUME_SET, SUPPORT_VOLUME_MUTE, SUPPORT_PREVIOUS_TRACK,
|
||||
SUPPORT_NEXT_TRACK, SUPPORT_TURN_ON, SUPPORT_TURN_OFF,
|
||||
MediaPlayerDevice, MEDIA_TYPE_MUSIC, MEDIA_TYPE_PLAYLIST, SUPPORT_PAUSE,
|
||||
SUPPORT_SEEK, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_MUTE,
|
||||
SUPPORT_PREVIOUS_TRACK, SUPPORT_NEXT_TRACK, SUPPORT_TURN_ON,
|
||||
SUPPORT_TURN_OFF, SUPPORT_PLAY_MEDIA,
|
||||
ATTR_ENTITY_PICTURE, ATTR_SUPPORTED_MEDIA_COMMANDS)
|
||||
from homeassistant.const import (
|
||||
STATE_IDLE, STATE_PLAYING, STATE_PAUSED, STATE_OFF, STATE_ON)
|
||||
|
@ -47,7 +48,8 @@ import requests
|
|||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SUPPORT_ITUNES = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
|
||||
SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_SEEK
|
||||
SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_SEEK | \
|
||||
SUPPORT_PLAY_MEDIA
|
||||
|
||||
SUPPORT_AIRPLAY = SUPPORT_VOLUME_SET | SUPPORT_TURN_ON | SUPPORT_TURN_OFF
|
||||
|
||||
|
@ -118,6 +120,20 @@ class Itunes(object):
|
|||
""" Skips back and returns the current state. """
|
||||
return self._command('previous')
|
||||
|
||||
def play_playlist(self, playlist_id_or_name):
|
||||
""" Sets a playlist to be current and returns the current state. """
|
||||
response = self._request('GET', '/playlists')
|
||||
playlists = response.get('playlists', [])
|
||||
|
||||
found_playlists = \
|
||||
[playlist for playlist in playlists if
|
||||
(playlist_id_or_name in [playlist["name"], playlist["id"]])]
|
||||
|
||||
if len(found_playlists) > 0:
|
||||
playlist = found_playlists[0]
|
||||
path = '/playlists/' + playlist['id'] + '/play'
|
||||
return self._request('PUT', path)
|
||||
|
||||
def artwork_url(self):
|
||||
""" Returns a URL of the current track's album art. """
|
||||
return self._base_url + '/artwork'
|
||||
|
@ -294,6 +310,11 @@ class ItunesDevice(MediaPlayerDevice):
|
|||
""" Album of current playing media. (Music track only) """
|
||||
return self.current_album
|
||||
|
||||
@property
|
||||
def media_playlist(self):
|
||||
""" Title of the currently playing playlist. """
|
||||
return self.current_playlist
|
||||
|
||||
@property
|
||||
def supported_media_commands(self):
|
||||
""" Flags of media commands that are supported. """
|
||||
|
@ -329,6 +350,12 @@ class ItunesDevice(MediaPlayerDevice):
|
|||
response = self.client.previous()
|
||||
self.update_state(response)
|
||||
|
||||
def play_media(self, media_type, media_id):
|
||||
""" play_media media player. """
|
||||
if media_type == MEDIA_TYPE_PLAYLIST:
|
||||
response = self.client.play_playlist(media_id)
|
||||
self.update_state(response)
|
||||
|
||||
|
||||
class AirPlayDevice(MediaPlayerDevice):
|
||||
""" Represents an AirPlay device via an iTunes-API instance. """
|
||||
|
|
|
@ -167,7 +167,7 @@ class KodiDevice(MediaPlayerDevice):
|
|||
def media_content_id(self):
|
||||
""" Content ID of current playing media. """
|
||||
if self._item is not None:
|
||||
return self._item['uniqueid']
|
||||
return self._item.get('uniqueid', None)
|
||||
|
||||
@property
|
||||
def media_content_type(self):
|
||||
|
|
|
@ -1,117 +1,148 @@
|
|||
"""
|
||||
homeassistant.components.media_player.plex
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Provides an interface to the Plex API.
|
||||
|
||||
Provides an interface to the Plex API
|
||||
|
||||
Configuration:
|
||||
|
||||
To use Plex add something like this to your configuration:
|
||||
|
||||
media_player:
|
||||
platform: plex
|
||||
name: plex_server
|
||||
user: plex
|
||||
password: my_secure_password
|
||||
|
||||
Variables:
|
||||
|
||||
name
|
||||
*Required
|
||||
The name of the backend device (Under Plex Media Server > settings > server).
|
||||
|
||||
user
|
||||
*Required
|
||||
The Plex username
|
||||
|
||||
password
|
||||
*Required
|
||||
The Plex password
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/media_player.plex.html
|
||||
"""
|
||||
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
from homeassistant.components.media_player import (
|
||||
MediaPlayerDevice, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK,
|
||||
SUPPORT_NEXT_TRACK, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO)
|
||||
from homeassistant.const import (
|
||||
STATE_IDLE, STATE_PLAYING, STATE_PAUSED, STATE_UNKNOWN)
|
||||
STATE_IDLE, STATE_PLAYING, STATE_PAUSED, STATE_OFF, STATE_UNKNOWN)
|
||||
import homeassistant.util as util
|
||||
|
||||
REQUIREMENTS = ['https://github.com/miniconfig/python-plexapi/archive/'
|
||||
'437e36dca3b7780dc0cb73941d662302c0cd2fa9.zip'
|
||||
REQUIREMENTS = ['https://github.com/adrienbrault/python-plexapi/archive/'
|
||||
'df2d0847e801d6d5cda920326d693cf75f304f1a.zip'
|
||||
'#python-plexapi==1.0.2']
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
|
||||
MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SUPPORT_PLEX = SUPPORT_PAUSE | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
# pylint: disable=unused-argument
|
||||
|
||||
|
||||
# pylint: disable=abstract-method, unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
""" Sets up the plex platform. """
|
||||
from plexapi.myplex import MyPlexUser
|
||||
from plexapi.exceptions import BadRequest
|
||||
|
||||
name = config.get('name', '')
|
||||
user = config.get('user', '')
|
||||
password = config.get('password', '')
|
||||
plexuser = MyPlexUser.signin(user, password)
|
||||
plexserver = plexuser.getResource(name).connect()
|
||||
dev = plexserver.clients()
|
||||
for device in dev:
|
||||
if "PlayStation" not in device.name:
|
||||
add_devices([PlexClient(device.name, plexserver)])
|
||||
plex_clients = {}
|
||||
plex_sessions = {}
|
||||
|
||||
@util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS)
|
||||
def update_devices():
|
||||
""" Updates the devices objects. """
|
||||
try:
|
||||
devices = plexuser.devices()
|
||||
except BadRequest:
|
||||
_LOGGER.exception("Error listing plex devices")
|
||||
return
|
||||
|
||||
new_plex_clients = []
|
||||
for device in devices:
|
||||
if (all(x not in ['client', 'player'] for x in device.provides)
|
||||
or 'PlexAPI' == device.product):
|
||||
continue
|
||||
|
||||
if device.clientIdentifier not in plex_clients:
|
||||
new_client = PlexClient(device, plex_sessions, update_devices,
|
||||
update_sessions)
|
||||
plex_clients[device.clientIdentifier] = new_client
|
||||
new_plex_clients.append(new_client)
|
||||
else:
|
||||
plex_clients[device.clientIdentifier].set_device(device)
|
||||
|
||||
if new_plex_clients:
|
||||
add_devices(new_plex_clients)
|
||||
|
||||
@util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS)
|
||||
def update_sessions():
|
||||
""" Updates the sessions objects. """
|
||||
try:
|
||||
sessions = plexserver.sessions()
|
||||
except BadRequest:
|
||||
_LOGGER.exception("Error listing plex sessions")
|
||||
return
|
||||
|
||||
plex_sessions.clear()
|
||||
for session in sessions:
|
||||
plex_sessions[session.player.machineIdentifier] = session
|
||||
|
||||
update_devices()
|
||||
update_sessions()
|
||||
|
||||
|
||||
class PlexClient(MediaPlayerDevice):
|
||||
""" Represents a Plex device. """
|
||||
|
||||
# pylint: disable=too-many-public-methods
|
||||
def __init__(self, device, plex_sessions, update_devices, update_sessions):
|
||||
self.plex_sessions = plex_sessions
|
||||
self.update_devices = update_devices
|
||||
self.update_sessions = update_sessions
|
||||
self.set_device(device)
|
||||
|
||||
def __init__(self, name, plexserver):
|
||||
self.client = plexserver.client(name)
|
||||
self._name = name
|
||||
self._media = None
|
||||
self.update()
|
||||
self.server = plexserver
|
||||
def set_device(self, device):
|
||||
""" Sets the device property. """
|
||||
self.device = device
|
||||
|
||||
@property
|
||||
def session(self):
|
||||
""" Returns the session, if any. """
|
||||
if self.device.clientIdentifier not in self.plex_sessions:
|
||||
return None
|
||||
|
||||
return self.plex_sessions[self.device.clientIdentifier]
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" Returns the name of the device. """
|
||||
return self._name
|
||||
return self.device.name or self.device.product or self.device.device
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
""" Returns the state of the device. """
|
||||
if self._media is None:
|
||||
return STATE_IDLE
|
||||
else:
|
||||
state = self._media.get('state')
|
||||
if self.session:
|
||||
state = self.session.player.state
|
||||
if state == 'playing':
|
||||
return STATE_PLAYING
|
||||
elif state == 'paused':
|
||||
return STATE_PAUSED
|
||||
elif self.device.isReachable:
|
||||
return STATE_IDLE
|
||||
else:
|
||||
return STATE_OFF
|
||||
|
||||
return STATE_UNKNOWN
|
||||
|
||||
def update(self):
|
||||
timeline = self.client.timeline()
|
||||
for timeline_item in timeline:
|
||||
if timeline_item.get('state') in ('playing', 'paused'):
|
||||
self._media = timeline_item
|
||||
self.update_devices(no_throttle=True)
|
||||
self.update_sessions(no_throttle=True)
|
||||
|
||||
@property
|
||||
def media_content_id(self):
|
||||
""" Content ID of current playing media. """
|
||||
if self._media is not None:
|
||||
return self._media.get('ratingKey')
|
||||
if self.session is not None:
|
||||
return self.session.ratingKey
|
||||
|
||||
@property
|
||||
def media_content_type(self):
|
||||
""" Content type of current playing media. """
|
||||
if self._media is None:
|
||||
if self.session is None:
|
||||
return None
|
||||
media_type = self.server.library.getByKey(
|
||||
self.media_content_id).type
|
||||
media_type = self.session.type
|
||||
if media_type == 'episode':
|
||||
return MEDIA_TYPE_TVSHOW
|
||||
elif media_type == 'movie':
|
||||
|
@ -121,50 +152,42 @@ class PlexClient(MediaPlayerDevice):
|
|||
@property
|
||||
def media_duration(self):
|
||||
""" Duration of current playing media in seconds. """
|
||||
if self._media is not None:
|
||||
total_time = self._media.get('duration')
|
||||
return total_time
|
||||
if self.session is not None:
|
||||
return self.session.duration
|
||||
|
||||
@property
|
||||
def media_image_url(self):
|
||||
""" Image url of current playing media. """
|
||||
if self._media is not None:
|
||||
return self.server.library.getByKey(self.media_content_id).thumbUrl
|
||||
return None
|
||||
if self.session is not None:
|
||||
return self.session.thumbUrl
|
||||
|
||||
@property
|
||||
def media_title(self):
|
||||
""" Title of current playing media. """
|
||||
# find a string we can use as a title
|
||||
if self._media is not None:
|
||||
return self.server.library.getByKey(self.media_content_id).title
|
||||
if self.session is not None:
|
||||
return self.session.title
|
||||
|
||||
@property
|
||||
def media_season(self):
|
||||
""" Season of curent playing media. (TV Show only) """
|
||||
if self._media is not None:
|
||||
show_season = self.server.library.getByKey(
|
||||
self.media_content_id).season().index
|
||||
return show_season
|
||||
return None
|
||||
""" Season of curent playing media (TV Show only). """
|
||||
from plexapi.video import Show
|
||||
if isinstance(self.session, Show):
|
||||
return self.session.seasons()[0].index
|
||||
|
||||
@property
|
||||
def media_series_title(self):
|
||||
""" Series title of current playing media. (TV Show only)"""
|
||||
if self._media is not None:
|
||||
series_title = self.server.library.getByKey(
|
||||
self.media_content_id).show().title
|
||||
return series_title
|
||||
return None
|
||||
""" Series title of current playing media (TV Show only). """
|
||||
from plexapi.video import Show
|
||||
if isinstance(self.session, Show):
|
||||
return self.session.grandparentTitle
|
||||
|
||||
@property
|
||||
def media_episode(self):
|
||||
""" Episode of current playing media. (TV Show only) """
|
||||
if self._media is not None:
|
||||
show_episode = self.server.library.getByKey(
|
||||
self.media_content_id).index
|
||||
return show_episode
|
||||
return None
|
||||
""" Episode of current playing media (TV Show only). """
|
||||
from plexapi.video import Show
|
||||
if isinstance(self.session, Show):
|
||||
return self.session.index
|
||||
|
||||
@property
|
||||
def supported_media_commands(self):
|
||||
|
@ -173,16 +196,16 @@ class PlexClient(MediaPlayerDevice):
|
|||
|
||||
def media_play(self):
|
||||
""" media_play media player. """
|
||||
self.client.play()
|
||||
self.device.play({'type': 'video'})
|
||||
|
||||
def media_pause(self):
|
||||
""" media_pause media player. """
|
||||
self.client.pause()
|
||||
self.device.pause({'type': 'video'})
|
||||
|
||||
def media_next_track(self):
|
||||
""" Send next track command. """
|
||||
self.client.skipNext()
|
||||
self.device.skipNext({'type': 'video'})
|
||||
|
||||
def media_previous_track(self):
|
||||
""" Send previous track command. """
|
||||
self.client.skipPrevious()
|
||||
self.device.skipPrevious({'type': 'video'})
|
||||
|
|
|
@ -23,6 +23,7 @@ mqtt:
|
|||
keepalive: 60
|
||||
username: your_username
|
||||
password: your_secret_password
|
||||
certificate: /home/paulus/dev/addtrustexternalcaroot.crt
|
||||
|
||||
Variables:
|
||||
|
||||
|
@ -42,8 +43,13 @@ Default is a random generated one.
|
|||
keepalive
|
||||
*Optional
|
||||
The keep alive in seconds for this client. Default is 60.
|
||||
|
||||
certificate
|
||||
*Optional
|
||||
Certificate to use for encrypting the connection to the broker.
|
||||
"""
|
||||
import logging
|
||||
import os
|
||||
import socket
|
||||
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
|
@ -74,6 +80,7 @@ CONF_CLIENT_ID = 'client_id'
|
|||
CONF_KEEPALIVE = 'keepalive'
|
||||
CONF_USERNAME = 'username'
|
||||
CONF_PASSWORD = 'password'
|
||||
CONF_CERTIFICATE = 'certificate'
|
||||
|
||||
ATTR_TOPIC = 'topic'
|
||||
ATTR_PAYLOAD = 'payload'
|
||||
|
@ -119,11 +126,18 @@ def setup(hass, config):
|
|||
keepalive = util.convert(conf.get(CONF_KEEPALIVE), int, DEFAULT_KEEPALIVE)
|
||||
username = util.convert(conf.get(CONF_USERNAME), str)
|
||||
password = util.convert(conf.get(CONF_PASSWORD), str)
|
||||
certificate = util.convert(conf.get(CONF_CERTIFICATE), str)
|
||||
|
||||
# For cloudmqtt.com, secured connection, auto fill in certificate
|
||||
if certificate is None and 19999 < port < 30000 and \
|
||||
broker.endswith('.cloudmqtt.com'):
|
||||
certificate = os.path.join(os.path.dirname(__file__),
|
||||
'addtrustexternalcaroot.crt')
|
||||
|
||||
global MQTT_CLIENT
|
||||
try:
|
||||
MQTT_CLIENT = MQTT(hass, broker, port, client_id, keepalive, username,
|
||||
password)
|
||||
password, certificate)
|
||||
except socket.error:
|
||||
_LOGGER.exception("Can't connect to the broker. "
|
||||
"Please check your settings and the broker "
|
||||
|
@ -161,7 +175,7 @@ def setup(hass, config):
|
|||
class MQTT(object): # pragma: no cover
|
||||
""" Implements messaging service for MQTT. """
|
||||
def __init__(self, hass, broker, port, client_id, keepalive, username,
|
||||
password):
|
||||
password, certificate):
|
||||
import paho.mqtt.client as mqtt
|
||||
|
||||
self.hass = hass
|
||||
|
@ -172,8 +186,12 @@ class MQTT(object): # pragma: no cover
|
|||
self._mqttc = mqtt.Client()
|
||||
else:
|
||||
self._mqttc = mqtt.Client(client_id)
|
||||
|
||||
if username is not None:
|
||||
self._mqttc.username_pw_set(username, password)
|
||||
if certificate is not None:
|
||||
self._mqttc.tls_set(certificate)
|
||||
|
||||
self._mqttc.on_subscribe = self._mqtt_on_subscribe
|
||||
self._mqttc.on_unsubscribe = self._mqtt_on_unsubscribe
|
||||
self._mqttc.on_connect = self._mqtt_on_connect
|
||||
|
@ -209,6 +227,17 @@ class MQTT(object): # pragma: no cover
|
|||
|
||||
def _mqtt_on_connect(self, mqttc, obj, flags, result_code):
|
||||
""" On connect, resubscribe to all topics we were subscribed to. """
|
||||
if result_code != 0:
|
||||
_LOGGER.error('Unable to connect to the MQTT broker: %s', {
|
||||
1: 'Incorrect protocol version',
|
||||
2: 'Invalid client identifier',
|
||||
3: 'Server unavailable',
|
||||
4: 'Bad username or password',
|
||||
5: 'Not authorised'
|
||||
}.get(result_code))
|
||||
self._mqttc.disconnect()
|
||||
return
|
||||
|
||||
old_topics = self.topics
|
||||
self._progress = {}
|
||||
self.topics = {}
|
25
homeassistant/components/mqtt/addtrustexternalcaroot.crt
Normal file
25
homeassistant/components/mqtt/addtrustexternalcaroot.crt
Normal file
|
@ -0,0 +1,25 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIIENjCCAx6gAwIBAgIBATANBgkqhkiG9w0BAQUFADBvMQswCQYDVQQGEwJTRTEU
|
||||
MBIGA1UEChMLQWRkVHJ1c3QgQUIxJjAkBgNVBAsTHUFkZFRydXN0IEV4dGVybmFs
|
||||
IFRUUCBOZXR3b3JrMSIwIAYDVQQDExlBZGRUcnVzdCBFeHRlcm5hbCBDQSBSb290
|
||||
MB4XDTAwMDUzMDEwNDgzOFoXDTIwMDUzMDEwNDgzOFowbzELMAkGA1UEBhMCU0Ux
|
||||
FDASBgNVBAoTC0FkZFRydXN0IEFCMSYwJAYDVQQLEx1BZGRUcnVzdCBFeHRlcm5h
|
||||
bCBUVFAgTmV0d29yazEiMCAGA1UEAxMZQWRkVHJ1c3QgRXh0ZXJuYWwgQ0EgUm9v
|
||||
dDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALf3GjPm8gAELTngTlvt
|
||||
H7xsD821+iO2zt6bETOXpClMfZOfvUq8k+0DGuOPz+VtUFrWlymUWoCwSXrbLpX9
|
||||
uMq/NzgtHj6RQa1wVsfwTz/oMp50ysiQVOnGXw94nZpAPA6sYapeFI+eh6FqUNzX
|
||||
mk6vBbOmcZSccbNQYArHE504B4YCqOmoaSYYkKtMsE8jqzpPhNjfzp/haW+710LX
|
||||
a0Tkx63ubUFfclpxCDezeWWkWaCUN/cALw3CknLa0Dhy2xSoRcRdKn23tNbE7qzN
|
||||
E0S3ySvdQwAl+mG5aWpYIxG3pzOPVnVZ9c0p10a3CitlttNCbxWyuHv77+ldU9U0
|
||||
WicCAwEAAaOB3DCB2TAdBgNVHQ4EFgQUrb2YejS0Jvf6xCZU7wO94CTLVBowCwYD
|
||||
VR0PBAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wgZkGA1UdIwSBkTCBjoAUrb2YejS0
|
||||
Jvf6xCZU7wO94CTLVBqhc6RxMG8xCzAJBgNVBAYTAlNFMRQwEgYDVQQKEwtBZGRU
|
||||
cnVzdCBBQjEmMCQGA1UECxMdQWRkVHJ1c3QgRXh0ZXJuYWwgVFRQIE5ldHdvcmsx
|
||||
IjAgBgNVBAMTGUFkZFRydXN0IEV4dGVybmFsIENBIFJvb3SCAQEwDQYJKoZIhvcN
|
||||
AQEFBQADggEBALCb4IUlwtYj4g+WBpKdQZic2YR5gdkeWxQHIzZlj7DYd7usQWxH
|
||||
YINRsPkyPef89iYTx4AWpb9a/IfPeHmJIZriTAcKhjW88t5RxNKWt9x+Tu5w/Rw5
|
||||
6wwCURQtjr0W4MHfRnXnJK3s9EK0hZNwEGe6nQY1ShjTK3rMUUKhemPR5ruhxSvC
|
||||
Nr4TDea9Y355e6cJDUCrat2PisP29owaQgVR1EX1n6diIWgVIEM8med8vSTYqZEX
|
||||
c4g/VhsxOBi0cQ+azcgOno4uG+GMmIPLHzHxREzGBHNJdmAPx/i9F4BrLunMTA5a
|
||||
mnkPIAou1Z5jJh5VkpTYghdae9C8x49OhgQ=
|
||||
-----END CERTIFICATE-----
|
|
@ -6,7 +6,9 @@ Provides functionality to notify people.
|
|||
"""
|
||||
from functools import partial
|
||||
import logging
|
||||
import os
|
||||
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.loader import get_component
|
||||
from homeassistant.helpers import config_per_platform
|
||||
|
||||
|
@ -36,6 +38,9 @@ def setup(hass, config):
|
|||
""" Sets up notify services. """
|
||||
success = False
|
||||
|
||||
descriptions = load_yaml_config_file(
|
||||
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
||||
|
||||
for platform, p_config in config_per_platform(config, DOMAIN, _LOGGER):
|
||||
# get platform
|
||||
notify_implementation = get_component(
|
||||
|
@ -69,7 +74,8 @@ def setup(hass, config):
|
|||
# register service
|
||||
service_call_handler = partial(notify_message, notify_service)
|
||||
service_notify = p_config.get(CONF_NAME, SERVICE_NOTIFY)
|
||||
hass.services.register(DOMAIN, service_notify, service_call_handler)
|
||||
hass.services.register(DOMAIN, service_notify, service_call_handler,
|
||||
descriptions.get(service_notify))
|
||||
success = True
|
||||
|
||||
return success
|
||||
|
|
0
homeassistant/components/notify/services.yaml
Normal file
0
homeassistant/components/notify/services.yaml
Normal file
|
@ -1,15 +1,15 @@
|
|||
"""
|
||||
homeassistant.components.notify.mail
|
||||
homeassistant.components.notify.smtp
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Mail (SMTP) notification service.
|
||||
|
||||
Configuration:
|
||||
|
||||
To use the Mail notifier you will need to add something like the following
|
||||
To use the smtp notifier you will need to add something like the following
|
||||
to your configuration.yaml file.
|
||||
|
||||
notify:
|
||||
platform: mail
|
||||
platform: smtp
|
||||
server: MAIL_SERVER
|
||||
port: YOUR_SMTP_PORT
|
||||
sender: SENDER_EMAIL_ADDRESS
|
||||
|
@ -140,13 +140,19 @@ class MailNotificationService(BaseNotificationService):
|
|||
self.username = username
|
||||
self.password = password
|
||||
self.recipient = recipient
|
||||
self.tries = 2
|
||||
self.mail = None
|
||||
|
||||
self.connect()
|
||||
|
||||
def connect(self):
|
||||
""" Connect/Authenticate to SMTP Server """
|
||||
|
||||
self.mail = smtplib.SMTP(self._server, self._port)
|
||||
self.mail.ehlo_or_helo_if_needed()
|
||||
if self.starttls == 1:
|
||||
self.mail.starttls()
|
||||
self.mail.ehlo()
|
||||
|
||||
self.mail.login(self.username, self.password)
|
||||
|
||||
def send_message(self, message="", **kwargs):
|
||||
|
@ -160,4 +166,12 @@ class MailNotificationService(BaseNotificationService):
|
|||
msg['From'] = self._sender
|
||||
msg['X-Mailer'] = 'HomeAssistant'
|
||||
|
||||
self.mail.sendmail(self._sender, self.recipient, msg.as_string())
|
||||
for _ in range(self.tries):
|
||||
try:
|
||||
self.mail.sendmail(self._sender, self.recipient,
|
||||
msg.as_string())
|
||||
break
|
||||
except smtplib.SMTPException:
|
||||
_LOGGER.warning('SMTPException sending mail: '
|
||||
'retrying connection')
|
||||
self.connect()
|
||||
|
|
66
homeassistant/components/notify/telegram.py
Normal file
66
homeassistant/components/notify/telegram.py
Normal file
|
@ -0,0 +1,66 @@
|
|||
"""
|
||||
homeassistant.components.notify.telegram
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Telegram platform for notify component.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/notify.telegram.html
|
||||
"""
|
||||
import logging
|
||||
import urllib
|
||||
|
||||
from homeassistant.helpers import validate_config
|
||||
from homeassistant.components.notify import (
|
||||
DOMAIN, ATTR_TITLE, BaseNotificationService)
|
||||
from homeassistant.const import CONF_API_KEY
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
REQUIREMENTS = ['python-telegram-bot==2.8.7']
|
||||
|
||||
|
||||
def get_service(hass, config):
|
||||
""" Get the Telegram notification service. """
|
||||
|
||||
if not validate_config(config,
|
||||
{DOMAIN: [CONF_API_KEY, 'chat_id']},
|
||||
_LOGGER):
|
||||
return None
|
||||
|
||||
try:
|
||||
import telegram
|
||||
except ImportError:
|
||||
_LOGGER.exception(
|
||||
"Unable to import python-telegram-bot. "
|
||||
"Did you maybe not install the 'python-telegram-bot' package?")
|
||||
return None
|
||||
|
||||
try:
|
||||
bot = telegram.Bot(token=config[DOMAIN][CONF_API_KEY])
|
||||
username = bot.getMe()['username']
|
||||
_LOGGER.info("Telegram bot is' %s'", username)
|
||||
except urllib.error.HTTPError:
|
||||
_LOGGER.error("Please check your access token.")
|
||||
return None
|
||||
|
||||
return TelegramNotificationService(
|
||||
config[DOMAIN][CONF_API_KEY],
|
||||
config[DOMAIN]['chat_id'])
|
||||
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
class TelegramNotificationService(BaseNotificationService):
|
||||
""" Implements notification service for Telegram. """
|
||||
|
||||
def __init__(self, api_key, chat_id):
|
||||
import telegram
|
||||
self._api_key = api_key
|
||||
self._chat_id = chat_id
|
||||
self.bot = telegram.Bot(token=self._api_key)
|
||||
|
||||
def send_message(self, message="", **kwargs):
|
||||
""" Send a message to a user. """
|
||||
|
||||
title = kwargs.get(ATTR_TITLE)
|
||||
|
||||
self.bot.sendMessage(chat_id=self._chat_id,
|
||||
text=title + " " + message)
|
89
homeassistant/components/rfxtrx.py
Normal file
89
homeassistant/components/rfxtrx.py
Normal file
|
@ -0,0 +1,89 @@
|
|||
"""
|
||||
homeassistant.components.rfxtrx
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Provides support for RFXtrx components.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/rfxtrx.html
|
||||
"""
|
||||
import logging
|
||||
from homeassistant.util import slugify
|
||||
|
||||
DEPENDENCIES = []
|
||||
REQUIREMENTS = ['https://github.com/Danielhiversen/pyRFXtrx/archive/0.2.zip' +
|
||||
'#RFXtrx==0.2']
|
||||
|
||||
DOMAIN = "rfxtrx"
|
||||
CONF_DEVICE = 'device'
|
||||
CONF_DEBUG = 'debug'
|
||||
RECEIVED_EVT_SUBSCRIBERS = []
|
||||
RFX_DEVICES = {}
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
RFXOBJECT = None
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
""" Setup the RFXtrx component. """
|
||||
|
||||
# Declare the Handle event
|
||||
def handle_receive(event):
|
||||
""" Callback all subscribers for RFXtrx gateway. """
|
||||
|
||||
# Log RFXCOM event
|
||||
entity_id = slugify(event.device.id_string.lower())
|
||||
packet_id = "".join("{0:02x}".format(x) for x in event.data)
|
||||
entity_name = "%s : %s" % (entity_id, packet_id)
|
||||
_LOGGER.info("Receive RFXCOM event from %s => %s",
|
||||
event.device, entity_name)
|
||||
|
||||
# Callback to HA registered components
|
||||
for subscriber in RECEIVED_EVT_SUBSCRIBERS:
|
||||
subscriber(event)
|
||||
|
||||
# Try to load the RFXtrx module
|
||||
try:
|
||||
import RFXtrx as rfxtrxmod
|
||||
except ImportError:
|
||||
_LOGGER.exception("Failed to import rfxtrx")
|
||||
return False
|
||||
|
||||
# Init the rfxtrx module
|
||||
global RFXOBJECT
|
||||
|
||||
if CONF_DEVICE not in config[DOMAIN]:
|
||||
_LOGGER.exception(
|
||||
"can found device parameter in %s YAML configuration section",
|
||||
DOMAIN
|
||||
)
|
||||
return False
|
||||
|
||||
device = config[DOMAIN][CONF_DEVICE]
|
||||
debug = config[DOMAIN].get(CONF_DEBUG, False)
|
||||
|
||||
RFXOBJECT = rfxtrxmod.Core(device, handle_receive, debug=debug)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def get_rfx_object(packetid):
|
||||
""" Return the RFXObject with the packetid. """
|
||||
try:
|
||||
import RFXtrx as rfxtrxmod
|
||||
except ImportError:
|
||||
_LOGGER.exception("Failed to import rfxtrx")
|
||||
return False
|
||||
|
||||
binarypacket = bytearray.fromhex(packetid)
|
||||
|
||||
pkt = rfxtrxmod.lowlevel.parse(binarypacket)
|
||||
if pkt is not None:
|
||||
if isinstance(pkt, rfxtrxmod.lowlevel.SensorPacket):
|
||||
obj = rfxtrxmod.SensorEvent(pkt)
|
||||
elif isinstance(pkt, rfxtrxmod.lowlevel.Status):
|
||||
obj = rfxtrxmod.StatusEvent(pkt)
|
||||
else:
|
||||
obj = rfxtrxmod.ControlEvent(pkt)
|
||||
|
||||
return obj
|
||||
|
||||
return None
|
|
@ -33,7 +33,7 @@ ATTR_ACTIVE_REQUESTED = "active_requested"
|
|||
|
||||
CONF_ENTITIES = "entities"
|
||||
|
||||
SceneConfig = namedtuple('SceneConfig', ['name', 'states'])
|
||||
SceneConfig = namedtuple('SceneConfig', ['name', 'states', 'fuzzy_match'])
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
|
@ -71,6 +71,15 @@ def setup(hass, config):
|
|||
def _process_config(scene_config):
|
||||
""" Process passed in config into a format to work with. """
|
||||
name = scene_config.get('name')
|
||||
|
||||
fuzzy_match = scene_config.get('fuzzy_match')
|
||||
if fuzzy_match:
|
||||
# default to 1%
|
||||
if isinstance(fuzzy_match, int):
|
||||
fuzzy_match /= 100.0
|
||||
else:
|
||||
fuzzy_match = 0.01
|
||||
|
||||
states = {}
|
||||
c_entities = dict(scene_config.get(CONF_ENTITIES, {}))
|
||||
|
||||
|
@ -91,7 +100,7 @@ def _process_config(scene_config):
|
|||
|
||||
states[entity_id.lower()] = State(entity_id, state, attributes)
|
||||
|
||||
return SceneConfig(name, states)
|
||||
return SceneConfig(name, states, fuzzy_match)
|
||||
|
||||
|
||||
class Scene(ToggleEntity):
|
||||
|
@ -179,9 +188,31 @@ class Scene(ToggleEntity):
|
|||
state = self.scene_config.states.get(cur_state and cur_state.entity_id)
|
||||
|
||||
return (cur_state is not None and state.state == cur_state.state and
|
||||
all(value == cur_state.attributes.get(key)
|
||||
all(self._compare_state_attribites(
|
||||
value, cur_state.attributes.get(key))
|
||||
for key, value in state.attributes.items()))
|
||||
|
||||
def _fuzzy_attribute_compare(self, attr_a, attr_b):
|
||||
"""
|
||||
Compare the attributes passed, use fuzzy logic if they are floats.
|
||||
"""
|
||||
|
||||
if not (isinstance(attr_a, float) and isinstance(attr_b, float)):
|
||||
return False
|
||||
diff = abs(attr_a - attr_b) / (abs(attr_a) + abs(attr_b))
|
||||
return diff <= self.scene_config.fuzzy_match
|
||||
|
||||
def _compare_state_attribites(self, attr1, attr2):
|
||||
""" Compare the attributes passed, using fuzzy logic if specified. """
|
||||
if attr1 == attr2:
|
||||
return True
|
||||
if not self.scene_config.fuzzy_match:
|
||||
return False
|
||||
if isinstance(attr1, list):
|
||||
return all(self._fuzzy_attribute_compare(a, b)
|
||||
for a, b in zip(attr1, attr2))
|
||||
return self._fuzzy_attribute_compare(attr1, attr2)
|
||||
|
||||
def _reproduce_state(self, states):
|
||||
""" Wraps reproduce state with Scence specific logic. """
|
||||
self.ignore_updates = True
|
||||
|
|
|
@ -3,51 +3,11 @@ homeassistant.components.sensor.arest
|
|||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
The arest sensor will consume an exposed aREST API of a device.
|
||||
|
||||
Configuration:
|
||||
|
||||
To use the arest sensor you will need to add something like the following
|
||||
to your configuration.yaml file.
|
||||
|
||||
sensor:
|
||||
platform: arest
|
||||
resource: http://IP_ADDRESS
|
||||
monitored_variables:
|
||||
- name: temperature
|
||||
unit: '°C'
|
||||
- name: humidity
|
||||
unit: '%'
|
||||
|
||||
Variables:
|
||||
|
||||
resource:
|
||||
*Required
|
||||
IP address of the device that is exposing an aREST API.
|
||||
|
||||
These are the variables for the monitored_variables array:
|
||||
|
||||
name
|
||||
*Required
|
||||
The name of the variable you wish to monitor.
|
||||
|
||||
unit
|
||||
*Optional
|
||||
Defines the units of measurement of the sensor, if any.
|
||||
|
||||
Details for the API: http://arest.io
|
||||
|
||||
Format of a default JSON response by aREST:
|
||||
{
|
||||
"variables":{
|
||||
"temperature":21,
|
||||
"humidity":89
|
||||
},
|
||||
"id":"device008",
|
||||
"name":"Bedroom",
|
||||
"connected":true
|
||||
}
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/sensor.arest.html
|
||||
"""
|
||||
import logging
|
||||
from requests import get, exceptions
|
||||
import requests
|
||||
from datetime import timedelta
|
||||
|
||||
from homeassistant.util import Throttle
|
||||
|
@ -58,36 +18,42 @@ _LOGGER = logging.getLogger(__name__)
|
|||
# Return cached results if last scan was less then this time ago
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)
|
||||
|
||||
CONF_RESOURCE = 'resource'
|
||||
CONF_MONITORED_VARIABLES = 'monitored_variables'
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
""" Get the aREST sensor. """
|
||||
|
||||
resource = config.get('resource', None)
|
||||
resource = config.get(CONF_RESOURCE)
|
||||
var_conf = config.get(CONF_MONITORED_VARIABLES)
|
||||
|
||||
if None in (resource, var_conf):
|
||||
_LOGGER.error('Not all required config keys present: %s',
|
||||
', '.join((CONF_RESOURCE, CONF_MONITORED_VARIABLES)))
|
||||
return False
|
||||
|
||||
try:
|
||||
response = get(resource, timeout=10)
|
||||
except exceptions.MissingSchema:
|
||||
response = requests.get(resource, timeout=10).json()
|
||||
except requests.exceptions.MissingSchema:
|
||||
_LOGGER.error("Missing resource or schema in configuration. "
|
||||
"Add http:// to your URL.")
|
||||
return False
|
||||
except exceptions.ConnectionError:
|
||||
except requests.exceptions.ConnectionError:
|
||||
_LOGGER.error("No route to device. "
|
||||
"Please check the IP address in the configuration file.")
|
||||
return False
|
||||
|
||||
rest = ArestData(resource)
|
||||
arest = ArestData(resource)
|
||||
|
||||
dev = []
|
||||
for variable in config['monitored_variables']:
|
||||
if 'unit' not in variable:
|
||||
variable['unit'] = ' '
|
||||
if variable['name'] not in response.json()['variables']:
|
||||
if variable['name'] not in response['variables']:
|
||||
_LOGGER.error('Variable: "%s" does not exist', variable['name'])
|
||||
else:
|
||||
dev.append(ArestSensor(rest,
|
||||
response.json()['name'],
|
||||
variable['name'],
|
||||
variable['unit']))
|
||||
continue
|
||||
|
||||
dev.append(ArestSensor(arest, response['name'], variable['name'],
|
||||
variable.get('unit')))
|
||||
|
||||
add_devices(dev)
|
||||
|
||||
|
@ -95,8 +61,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
class ArestSensor(Entity):
|
||||
""" Implements an aREST sensor. """
|
||||
|
||||
def __init__(self, rest, location, variable, unit_of_measurement):
|
||||
self.rest = rest
|
||||
def __init__(self, arest, location, variable, unit_of_measurement):
|
||||
self.arest = arest
|
||||
self._name = '{} {}'.format(location.title(), variable.title())
|
||||
self._variable = variable
|
||||
self._state = 'n/a'
|
||||
|
@ -116,17 +82,16 @@ class ArestSensor(Entity):
|
|||
@property
|
||||
def state(self):
|
||||
""" Returns the state of the device. """
|
||||
return self._state
|
||||
|
||||
def update(self):
|
||||
""" Gets the latest data from aREST API and updates the state. """
|
||||
self.rest.update()
|
||||
values = self.rest.data
|
||||
values = self.arest.data
|
||||
|
||||
if 'error' in values:
|
||||
self._state = values['error']
|
||||
return values['error']
|
||||
else:
|
||||
self._state = values[self._variable]
|
||||
return values.get(self._variable, 'n/a')
|
||||
|
||||
def update(self):
|
||||
""" Gets the latest data from aREST API. """
|
||||
self.arest.update()
|
||||
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
|
@ -135,16 +100,14 @@ class ArestData(object):
|
|||
|
||||
def __init__(self, resource):
|
||||
self.resource = resource
|
||||
self.data = dict()
|
||||
self.data = {}
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
""" Gets the latest data from aREST device. """
|
||||
try:
|
||||
response = get(self.resource, timeout=10)
|
||||
if 'error' in self.data:
|
||||
del self.data['error']
|
||||
response = requests.get(self.resource, timeout=10)
|
||||
self.data = response.json()['variables']
|
||||
except exceptions.ConnectionError:
|
||||
except requests.exceptions.ConnectionError:
|
||||
_LOGGER.error("No route to device. Is device offline?")
|
||||
self.data['error'] = 'n/a'
|
||||
self.data = {'error': 'error fetching'}
|
||||
|
|
|
@ -17,6 +17,26 @@ Variables:
|
|||
port
|
||||
*Required
|
||||
Port of your connection to your MySensors device.
|
||||
|
||||
debug
|
||||
*Optional
|
||||
Enable or disable verbose debug logging.
|
||||
|
||||
persistence
|
||||
*Optional
|
||||
Enable or disable local persistence of sensor information.
|
||||
Note: If this is disabled, then each sensor will need to send presentation
|
||||
messages after Home Assistant starts
|
||||
|
||||
persistence_file
|
||||
*Optional
|
||||
Path to a file to save sensor information.
|
||||
Note: The file extension determines the file type. Currently supported file
|
||||
types are 'pickle' and 'json'.
|
||||
|
||||
version
|
||||
*Optional
|
||||
Specifies the MySensors protocol version to use (ex. 1.4, 1.5).
|
||||
"""
|
||||
import logging
|
||||
|
||||
|
@ -30,14 +50,16 @@ from homeassistant.const import (
|
|||
CONF_PORT = "port"
|
||||
CONF_DEBUG = "debug"
|
||||
CONF_PERSISTENCE = "persistence"
|
||||
CONF_PERSISTENCE_FILE = "persistence_file"
|
||||
CONF_VERSION = "version"
|
||||
|
||||
ATTR_NODE_ID = "node_id"
|
||||
ATTR_CHILD_ID = "child_id"
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
REQUIREMENTS = ['https://github.com/theolind/pymysensors/archive/'
|
||||
'35b87d880147a34107da0d40cb815d75e6cb4af7.zip'
|
||||
'#pymysensors==0.2']
|
||||
'd4b809c2167650691058d1e29bfd2c4b1792b4b0.zip'
|
||||
'#pymysensors==0.3']
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
@ -86,9 +108,13 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
return False
|
||||
|
||||
persistence = config.get(CONF_PERSISTENCE, True)
|
||||
persistence_file = config.get(CONF_PERSISTENCE_FILE, 'mysensors.pickle')
|
||||
version = config.get(CONF_VERSION, '1.4')
|
||||
|
||||
gateway = mysensors.SerialGateway(port, sensor_update,
|
||||
persistence=persistence)
|
||||
persistence=persistence,
|
||||
persistence_file=persistence_file,
|
||||
protocol_version=version)
|
||||
gateway.metric = is_metric
|
||||
gateway.debug = config.get(CONF_DEBUG, False)
|
||||
gateway.start()
|
||||
|
|
198
homeassistant/components/sensor/rest.py
Normal file
198
homeassistant/components/sensor/rest.py
Normal file
|
@ -0,0 +1,198 @@
|
|||
"""
|
||||
homeassistant.components.sensor.rest
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
The rest sensor will consume JSON responses sent by an exposed REST API.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/sensor.rest.html
|
||||
"""
|
||||
import logging
|
||||
import requests
|
||||
from json import loads
|
||||
from datetime import timedelta
|
||||
|
||||
from homeassistant.util import Throttle
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_NAME = 'REST Sensor'
|
||||
DEFAULT_METHOD = 'GET'
|
||||
|
||||
# Return cached results if last scan was less then this time ago
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)
|
||||
|
||||
|
||||
# pylint: disable=unused-variable
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
""" Get the REST sensor. """
|
||||
|
||||
use_get = False
|
||||
use_post = False
|
||||
|
||||
resource = config.get('resource', None)
|
||||
method = config.get('method', DEFAULT_METHOD)
|
||||
payload = config.get('payload', None)
|
||||
verify_ssl = config.get('verify_ssl', True)
|
||||
|
||||
if method == 'GET':
|
||||
use_get = True
|
||||
elif method == 'POST':
|
||||
use_post = True
|
||||
|
||||
try:
|
||||
if use_get:
|
||||
response = requests.get(resource, timeout=10, verify=verify_ssl)
|
||||
elif use_post:
|
||||
response = requests.post(resource, data=payload, timeout=10,
|
||||
verify=verify_ssl)
|
||||
if not response.ok:
|
||||
_LOGGER.error('Response status is "%s"', response.status_code)
|
||||
return False
|
||||
except requests.exceptions.MissingSchema:
|
||||
_LOGGER.error('Missing resource or schema in configuration. '
|
||||
'Add http:// to your URL.')
|
||||
return False
|
||||
except requests.exceptions.ConnectionError:
|
||||
_LOGGER.error('No route to resource/endpoint. '
|
||||
'Please check the URL in the configuration file.')
|
||||
return False
|
||||
|
||||
try:
|
||||
data = loads(response.text)
|
||||
except ValueError:
|
||||
_LOGGER.error('No valid JSON in the response in: %s', data)
|
||||
return False
|
||||
|
||||
try:
|
||||
RestSensor.extract_value(data, config.get('variable'))
|
||||
except KeyError:
|
||||
_LOGGER.error('Variable "%s" not found in response: "%s"',
|
||||
config.get('variable'), data)
|
||||
return False
|
||||
|
||||
if use_get:
|
||||
rest = RestDataGet(resource, verify_ssl)
|
||||
elif use_post:
|
||||
rest = RestDataPost(resource, payload, verify_ssl)
|
||||
|
||||
add_devices([RestSensor(rest,
|
||||
config.get('name', DEFAULT_NAME),
|
||||
config.get('variable'),
|
||||
config.get('unit_of_measurement'),
|
||||
config.get('correction_factor', None),
|
||||
config.get('decimal_places', None))])
|
||||
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
class RestSensor(Entity):
|
||||
""" Implements a REST sensor. """
|
||||
|
||||
def __init__(self, rest, name, variable, unit_of_measurement, corr_factor,
|
||||
decimal_places):
|
||||
self.rest = rest
|
||||
self._name = name
|
||||
self._variable = variable
|
||||
self._state = 'n/a'
|
||||
self._unit_of_measurement = unit_of_measurement
|
||||
self._corr_factor = corr_factor
|
||||
self._decimal_places = decimal_places
|
||||
self.update()
|
||||
|
||||
@classmethod
|
||||
def extract_value(cls, data, variable):
|
||||
""" Extracts the value using a key name or a path. """
|
||||
if isinstance(variable, list):
|
||||
for variable_item in variable:
|
||||
data = data[variable_item]
|
||||
return data
|
||||
else:
|
||||
return data[variable]
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" The name of the sensor. """
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
""" Unit the value is expressed in. """
|
||||
return self._unit_of_measurement
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
""" Returns the state of the device. """
|
||||
return self._state
|
||||
|
||||
def update(self):
|
||||
""" Gets the latest data from REST API and updates the state. """
|
||||
self.rest.update()
|
||||
value = self.rest.data
|
||||
|
||||
if 'error' in value:
|
||||
self._state = value['error']
|
||||
else:
|
||||
try:
|
||||
if value is not None:
|
||||
value = RestSensor.extract_value(value, self._variable)
|
||||
if self._corr_factor is not None \
|
||||
and self._decimal_places is not None:
|
||||
self._state = round(
|
||||
(float(value) *
|
||||
float(self._corr_factor)),
|
||||
self._decimal_places)
|
||||
elif self._corr_factor is not None \
|
||||
and self._decimal_places is None:
|
||||
self._state = round(float(value) *
|
||||
float(self._corr_factor))
|
||||
else:
|
||||
self._state = value
|
||||
except ValueError:
|
||||
self._state = RestSensor.extract_value(value, self._variable)
|
||||
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
class RestDataGet(object):
|
||||
""" Class for handling the data retrieval with GET method. """
|
||||
|
||||
def __init__(self, resource, verify_ssl):
|
||||
self._resource = resource
|
||||
self._verify_ssl = verify_ssl
|
||||
self.data = dict()
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
""" Gets the latest data from REST service with GET method. """
|
||||
try:
|
||||
response = requests.get(self._resource, timeout=10,
|
||||
verify=self._verify_ssl)
|
||||
if 'error' in self.data:
|
||||
del self.data['error']
|
||||
self.data = response.json()
|
||||
except requests.exceptions.ConnectionError:
|
||||
_LOGGER.error("No route to resource/endpoint.")
|
||||
self.data['error'] = 'N/A'
|
||||
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
class RestDataPost(object):
|
||||
""" Class for handling the data retrieval with POST method. """
|
||||
|
||||
def __init__(self, resource, payload, verify_ssl):
|
||||
self._resource = resource
|
||||
self._payload = payload
|
||||
self._verify_ssl = verify_ssl
|
||||
self.data = dict()
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
""" Gets the latest data from REST service with POST method. """
|
||||
try:
|
||||
response = requests.post(self._resource, data=self._payload,
|
||||
timeout=10, verify=self._verify_ssl)
|
||||
if 'error' in self.data:
|
||||
del self.data['error']
|
||||
self.data = response.json()
|
||||
except requests.exceptions.ConnectionError:
|
||||
_LOGGER.error("No route to resource/endpoint.")
|
||||
self.data['error'] = 'N/A'
|
|
@ -3,30 +3,19 @@ homeassistant.components.sensor.rfxtrx
|
|||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Shows sensor values from RFXtrx sensors.
|
||||
|
||||
Configuration:
|
||||
|
||||
To use the rfxtrx sensors you will need to add something like the following to
|
||||
your configuration.yaml file.
|
||||
|
||||
sensor:
|
||||
platform: rfxtrx
|
||||
device: PATH_TO_DEVICE
|
||||
|
||||
Variables:
|
||||
|
||||
device
|
||||
*Required
|
||||
Path to your RFXtrx device.
|
||||
E.g. /dev/serial/by-id/usb-RFXCOM_RFXtrx433_A1Y0NJGR-if00-port0
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/sensor.rfxtrx.html
|
||||
"""
|
||||
import logging
|
||||
from collections import OrderedDict
|
||||
|
||||
from homeassistant.const import (TEMP_CELCIUS)
|
||||
from homeassistant.helpers.entity import Entity
|
||||
import homeassistant.components.rfxtrx as rfxtrx
|
||||
from RFXtrx import SensorEvent
|
||||
from homeassistant.util import slugify
|
||||
|
||||
REQUIREMENTS = ['https://github.com/Danielhiversen/pyRFXtrx/archive/' +
|
||||
'ec7a1aaddf8270db6e5da1c13d58c1547effd7cf.zip#RFXtrx==0.15']
|
||||
DEPENDENCIES = ['rfxtrx']
|
||||
|
||||
DATA_TYPES = OrderedDict([
|
||||
('Temperature', TEMP_CELCIUS),
|
||||
|
@ -34,32 +23,30 @@ DATA_TYPES = OrderedDict([
|
|||
('Barometer', ''),
|
||||
('Wind direction', ''),
|
||||
('Rain rate', '')])
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
""" Setup the RFXtrx platform. """
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
sensors = {} # keep track of sensors added to HA
|
||||
|
||||
def sensor_update(event):
|
||||
""" Callback for sensor updates from the RFXtrx gateway. """
|
||||
if event.device.id_string in sensors:
|
||||
sensors[event.device.id_string].event = event
|
||||
else:
|
||||
logger.info("adding new sensor: %s", event.device.type_string)
|
||||
new_sensor = RfxtrxSensor(event)
|
||||
sensors[event.device.id_string] = new_sensor
|
||||
add_devices([new_sensor])
|
||||
try:
|
||||
import RFXtrx as rfxtrx
|
||||
except ImportError:
|
||||
logger.exception(
|
||||
"Failed to import rfxtrx")
|
||||
return False
|
||||
if isinstance(event.device, SensorEvent):
|
||||
entity_id = slugify(event.device.id_string.lower())
|
||||
|
||||
device = config.get("device", "")
|
||||
rfxtrx.Core(device, sensor_update)
|
||||
# Add entity if not exist and the automatic_add is True
|
||||
if entity_id not in rfxtrx.RFX_DEVICES:
|
||||
automatic_add = config.get('automatic_add', True)
|
||||
if automatic_add:
|
||||
_LOGGER.info("Automatic add %s rfxtrx.sensor", entity_id)
|
||||
new_sensor = RfxtrxSensor(event)
|
||||
rfxtrx.RFX_DEVICES[entity_id] = new_sensor
|
||||
add_devices_callback([new_sensor])
|
||||
else:
|
||||
rfxtrx.RFX_DEVICES[entity_id].event = event
|
||||
|
||||
if sensor_update not in rfxtrx.RECEIVED_EVT_SUBSCRIBERS:
|
||||
rfxtrx.RECEIVED_EVT_SUBSCRIBERS.append(sensor_update)
|
||||
|
||||
|
||||
class RfxtrxSensor(Entity):
|
||||
|
@ -67,7 +54,6 @@ class RfxtrxSensor(Entity):
|
|||
|
||||
def __init__(self, event):
|
||||
self.event = event
|
||||
|
||||
self._unit_of_measurement = None
|
||||
self._data_type = None
|
||||
for data_type in DATA_TYPES:
|
||||
|
@ -86,13 +72,14 @@ class RfxtrxSensor(Entity):
|
|||
|
||||
@property
|
||||
def state(self):
|
||||
""" Returns the state of the device. """
|
||||
if self._data_type:
|
||||
return self.event.values[self._data_type]
|
||||
return None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" Get the mame of the sensor. """
|
||||
""" Get the name of the sensor. """
|
||||
return self._name
|
||||
|
||||
@property
|
||||
|
|
|
@ -3,7 +3,8 @@
|
|||
homeassistant.components.sensor.rpi_gpio
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Allows to configure a binary state sensor using RPi GPIO.
|
||||
Note: To use RPi GPIO, Home Assistant must be run as root.
|
||||
To avoid having to run Home Assistant as root when using this component,
|
||||
run a Raspbian version released at or after September 29, 2015.
|
||||
|
||||
sensor:
|
||||
platform: rpi_gpio
|
||||
|
|
|
@ -34,7 +34,7 @@ import homeassistant.util as util
|
|||
|
||||
DatatypeDescription = namedtuple("DatatypeDescription", ['name', 'unit'])
|
||||
|
||||
REQUIREMENTS = ['tellcore-py==1.0.4']
|
||||
REQUIREMENTS = ['tellcore-py==1.1.2']
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
|
|
80
homeassistant/components/sensor/worldclock.py
Normal file
80
homeassistant/components/sensor/worldclock.py
Normal file
|
@ -0,0 +1,80 @@
|
|||
"""
|
||||
homeassistant.components.sensor.worldclock
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
The Worldclock sensor let you display the current time of a different time
|
||||
zone.
|
||||
|
||||
Configuration:
|
||||
|
||||
To use the Worldclock sensor you will need to add something like the
|
||||
following to your configuration.yaml file.
|
||||
|
||||
sensor:
|
||||
platform: worldclock
|
||||
time_zone: America/New_York
|
||||
name: New York
|
||||
|
||||
Variables:
|
||||
|
||||
time_zone
|
||||
*Required
|
||||
Time zone you want to display.
|
||||
|
||||
name
|
||||
*Optional
|
||||
Name of the sensor to use in the frontend.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/sensor.worldclock.html
|
||||
"""
|
||||
import logging
|
||||
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
DEFAULT_NAME = "Worldclock Sensor"
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
""" Get the Worldclock sensor. """
|
||||
|
||||
try:
|
||||
time_zone = dt_util.get_time_zone(config.get('time_zone'))
|
||||
except AttributeError:
|
||||
_LOGGER.error("time_zone in platform configuration is missing.")
|
||||
return False
|
||||
|
||||
if time_zone is None:
|
||||
_LOGGER.error("Timezone '%s' is not valid.", config.get('time_zone'))
|
||||
return False
|
||||
|
||||
add_devices([WorldClockSensor(
|
||||
time_zone,
|
||||
config.get('name', DEFAULT_NAME)
|
||||
)])
|
||||
|
||||
|
||||
class WorldClockSensor(Entity):
|
||||
""" Implements a Worldclock sensor. """
|
||||
|
||||
def __init__(self, time_zone, name):
|
||||
self._name = name
|
||||
self._time_zone = time_zone
|
||||
self._state = None
|
||||
self.update()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" Returns the name of the device. """
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
""" Returns the state of the device. """
|
||||
return self._state
|
||||
|
||||
def update(self):
|
||||
""" Gets the time and updates the states. """
|
||||
self._state = dt_util.datetime_to_time_str(
|
||||
dt_util.now(time_zone=self._time_zone))
|
48
homeassistant/components/shell_command.py
Normal file
48
homeassistant/components/shell_command.py
Normal file
|
@ -0,0 +1,48 @@
|
|||
"""
|
||||
homeassistant.components.shell_command
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Component to expose shell commands as services.
|
||||
|
||||
shell_command:
|
||||
restart_pow: touch ~/.pow/restart.txt
|
||||
|
||||
"""
|
||||
import logging
|
||||
import subprocess
|
||||
|
||||
from homeassistant.util import slugify
|
||||
|
||||
DOMAIN = 'shell_command'
|
||||
DEPENDENCIES = []
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
""" Sets up the shell_command component. """
|
||||
conf = config.get(DOMAIN)
|
||||
|
||||
if not isinstance(conf, dict):
|
||||
_LOGGER.error('Expected configuration to be a dictionary')
|
||||
return False
|
||||
|
||||
for name in conf.keys():
|
||||
if name != slugify(name):
|
||||
_LOGGER.error('Invalid service name: %s. Try %s',
|
||||
name, slugify(name))
|
||||
return False
|
||||
|
||||
def service_handler(call):
|
||||
""" Execute a shell command service. """
|
||||
try:
|
||||
subprocess.call(conf[call.service].split(' '),
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL)
|
||||
except subprocess.SubprocessError:
|
||||
_LOGGER.exception('Error running command')
|
||||
|
||||
for name in conf.keys():
|
||||
hass.services.register(DOMAIN, name, service_handler)
|
||||
|
||||
return True
|
|
@ -3,9 +3,11 @@ homeassistant.components.switch
|
|||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Component to interface with various switches that can be controlled remotely.
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
import os
|
||||
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.entity import ToggleEntity
|
||||
|
||||
|
@ -83,8 +85,12 @@ def setup(hass, config):
|
|||
if switch.should_poll:
|
||||
switch.update_ha_state(True)
|
||||
|
||||
hass.services.register(DOMAIN, SERVICE_TURN_OFF, handle_switch_service)
|
||||
hass.services.register(DOMAIN, SERVICE_TURN_ON, handle_switch_service)
|
||||
descriptions = load_yaml_config_file(
|
||||
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
||||
hass.services.register(DOMAIN, SERVICE_TURN_OFF, handle_switch_service,
|
||||
descriptions.get(SERVICE_TURN_OFF))
|
||||
hass.services.register(DOMAIN, SERVICE_TURN_ON, handle_switch_service,
|
||||
descriptions.get(SERVICE_TURN_ON))
|
||||
|
||||
return True
|
||||
|
||||
|
|
|
@ -3,38 +3,9 @@ homeassistant.components.switch.arest
|
|||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
The arest switch can control the digital pins of a device running with the
|
||||
aREST RESTful framework for Arduino, the ESP8266, and the Raspberry Pi.
|
||||
Only tested with Arduino boards so far.
|
||||
|
||||
Configuration:
|
||||
|
||||
To use the arest switch you will need to add something like the following
|
||||
to your configuration.yaml file.
|
||||
|
||||
sensor:
|
||||
platform: arest
|
||||
resource: http://IP_ADDRESS
|
||||
pins:
|
||||
11:
|
||||
name: Fan Office
|
||||
12:
|
||||
name: Light Desk
|
||||
|
||||
Variables:
|
||||
|
||||
resource:
|
||||
*Required
|
||||
IP address of the device that is exposing an aREST API.
|
||||
|
||||
pins:
|
||||
The number of the digital pin to switch.
|
||||
|
||||
These are the variables for the pins array:
|
||||
|
||||
name
|
||||
*Required
|
||||
The name for the pin that will be used in the frontend.
|
||||
|
||||
Details for the API: http://arest.io
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/switch.arest.html
|
||||
"""
|
||||
import logging
|
||||
from requests import get, exceptions
|
||||
|
|
112
homeassistant/components/switch/rfxtrx.py
Normal file
112
homeassistant/components/switch/rfxtrx.py
Normal file
|
@ -0,0 +1,112 @@
|
|||
"""
|
||||
homeassistant.components.switch.rfxtrx
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Support for RFXtrx switches.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/switch.rfxtrx.html
|
||||
"""
|
||||
import logging
|
||||
import homeassistant.components.rfxtrx as rfxtrx
|
||||
from RFXtrx import LightingDevice
|
||||
|
||||
from homeassistant.components.switch import SwitchDevice
|
||||
from homeassistant.util import slugify
|
||||
|
||||
DEPENDENCIES = ['rfxtrx']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
""" Setup the RFXtrx platform. """
|
||||
|
||||
# Add switch from config file
|
||||
switchs = []
|
||||
devices = config.get('devices')
|
||||
if devices:
|
||||
for entity_id, entity_info in devices.items():
|
||||
if entity_id not in rfxtrx.RFX_DEVICES:
|
||||
_LOGGER.info("Add %s rfxtrx.switch", entity_info['name'])
|
||||
rfxobject = rfxtrx.get_rfx_object(entity_info['packetid'])
|
||||
newswitch = RfxtrxSwitch(entity_info['name'], rfxobject, False)
|
||||
rfxtrx.RFX_DEVICES[entity_id] = newswitch
|
||||
switchs.append(newswitch)
|
||||
|
||||
add_devices_callback(switchs)
|
||||
|
||||
def switch_update(event):
|
||||
""" Callback for sensor updates from the RFXtrx gateway. """
|
||||
if isinstance(event.device, LightingDevice):
|
||||
return
|
||||
|
||||
# Add entity if not exist and the automatic_add is True
|
||||
entity_id = slugify(event.device.id_string.lower())
|
||||
if entity_id not in rfxtrx.RFX_DEVICES:
|
||||
automatic_add = config.get('automatic_add', False)
|
||||
if not automatic_add:
|
||||
return
|
||||
|
||||
_LOGGER.info(
|
||||
"Automatic add %s rfxtrx.switch (Class: %s Sub: %s)",
|
||||
entity_id,
|
||||
event.device.__class__.__name__,
|
||||
event.device.subtype
|
||||
)
|
||||
pkt_id = "".join("{0:02x}".format(x) for x in event.data)
|
||||
entity_name = "%s : %s" % (entity_id, pkt_id)
|
||||
new_switch = RfxtrxSwitch(entity_name, event, False)
|
||||
rfxtrx.RFX_DEVICES[entity_id] = new_switch
|
||||
add_devices_callback([new_switch])
|
||||
|
||||
# Check if entity exists or previously added automatically
|
||||
if entity_id in rfxtrx.RFX_DEVICES:
|
||||
if event.values['Command'] == 'On'\
|
||||
or event.values['Command'] == 'Off':
|
||||
if event.values['Command'] == 'On':
|
||||
rfxtrx.RFX_DEVICES[entity_id].turn_on()
|
||||
else:
|
||||
rfxtrx.RFX_DEVICES[entity_id].turn_off()
|
||||
|
||||
# Subscribe to main rfxtrx events
|
||||
if switch_update not in rfxtrx.RECEIVED_EVT_SUBSCRIBERS:
|
||||
rfxtrx.RECEIVED_EVT_SUBSCRIBERS.append(switch_update)
|
||||
|
||||
|
||||
class RfxtrxSwitch(SwitchDevice):
|
||||
""" Provides a RFXtrx switch. """
|
||||
def __init__(self, name, event, state):
|
||||
self._name = name
|
||||
self._event = event
|
||||
self._state = state
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
""" No polling needed for a RFXtrx switch. """
|
||||
return False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" Returns the name of the device if any. """
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
""" True if device is on. """
|
||||
return self._state
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
""" Turn the device on. """
|
||||
if self._event:
|
||||
self._event.device.send_on(rfxtrx.RFXOBJECT.transport)
|
||||
|
||||
self._state = True
|
||||
self.update_ha_state()
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
""" Turn the device off. """
|
||||
if self._event:
|
||||
self._event.device.send_off(rfxtrx.RFXOBJECT.transport)
|
||||
|
||||
self._state = False
|
||||
self.update_ha_state()
|
0
homeassistant/components/switch/services.yaml
Normal file
0
homeassistant/components/switch/services.yaml
Normal file
|
@ -11,13 +11,14 @@ signal_repetitions: 3
|
|||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.const import ATTR_FRIENDLY_NAME
|
||||
from homeassistant.const import (EVENT_HOMEASSISTANT_STOP,
|
||||
ATTR_FRIENDLY_NAME)
|
||||
from homeassistant.helpers.entity import ToggleEntity
|
||||
import tellcore.constants as tellcore_constants
|
||||
from tellcore.library import DirectCallbackDispatcher
|
||||
SINGAL_REPETITIONS = 1
|
||||
|
||||
REQUIREMENTS = ['tellcore-py==1.0.4']
|
||||
REQUIREMENTS = ['tellcore-py==1.1.2']
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
|
@ -30,12 +31,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
|||
"Failed to import tellcore")
|
||||
return
|
||||
|
||||
# pylint: disable=no-member
|
||||
if telldus.TelldusCore.callback_dispatcher is None:
|
||||
dispatcher = DirectCallbackDispatcher()
|
||||
core = telldus.TelldusCore(callback_dispatcher=dispatcher)
|
||||
else:
|
||||
core = telldus.TelldusCore()
|
||||
core = telldus.TelldusCore(callback_dispatcher=DirectCallbackDispatcher())
|
||||
|
||||
signal_repetitions = config.get('signal_repetitions', SINGAL_REPETITIONS)
|
||||
|
||||
|
@ -52,9 +48,17 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
|||
""" Called from the TelldusCore library to update one device """
|
||||
for switch_device in switches:
|
||||
if switch_device.tellstick_device.id == id_:
|
||||
switch_device.update_ha_state(True)
|
||||
switch_device.update_ha_state()
|
||||
break
|
||||
|
||||
core.register_device_event(_device_event_callback)
|
||||
callback_id = core.register_device_event(_device_event_callback)
|
||||
|
||||
def unload_telldus_lib(event):
|
||||
""" Un-register the callback bindings """
|
||||
if callback_id is not None:
|
||||
core.unregister_callback(callback_id)
|
||||
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, unload_telldus_lib)
|
||||
|
||||
add_devices_callback(switches)
|
||||
|
||||
|
|
|
@ -122,7 +122,7 @@ class VeraSwitch(ToggleEntity):
|
|||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
attr = super().state_attributes
|
||||
attr = super().state_attributes or {}
|
||||
|
||||
if self.vera_device.has_battery:
|
||||
attr[ATTR_BATTERY_LEVEL] = self.vera_device.battery_level + '%'
|
||||
|
|
|
@ -9,7 +9,7 @@ import logging
|
|||
from homeassistant.components.switch import SwitchDevice
|
||||
from homeassistant.const import STATE_ON, STATE_OFF, STATE_STANDBY
|
||||
|
||||
REQUIREMENTS = ['pywemo==0.3']
|
||||
REQUIREMENTS = ['pywemo==0.3.1']
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
|
@ -123,9 +123,14 @@ class WemoSwitch(SwitchDevice):
|
|||
|
||||
def update(self):
|
||||
""" Update WeMo state. """
|
||||
self.wemo.get_state(True)
|
||||
if self.wemo.model_name == 'Insight':
|
||||
self.insight_params = self.wemo.insight_params
|
||||
self.insight_params['standby_state'] = self.wemo.get_standby_state
|
||||
elif self.wemo.model_name == 'Maker':
|
||||
self.maker_params = self.wemo.maker_params
|
||||
try:
|
||||
self.wemo.get_state(True)
|
||||
if self.wemo.model_name == 'Insight':
|
||||
self.insight_params = self.wemo.insight_params
|
||||
self.insight_params['standby_state'] = (
|
||||
self.wemo.get_standby_state)
|
||||
elif self.wemo.model_name == 'Maker':
|
||||
self.maker_params = self.wemo.maker_params
|
||||
except AttributeError:
|
||||
logging.getLogger(__name__).warning(
|
||||
'Could not update status for %s', self.name)
|
||||
|
|
|
@ -5,9 +5,11 @@ homeassistant.components.thermostat
|
|||
Provides functionality to interact with thermostats.
|
||||
"""
|
||||
import logging
|
||||
import os
|
||||
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
import homeassistant.util as util
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.temperature import convert
|
||||
|
@ -101,11 +103,16 @@ def setup(hass, config):
|
|||
for thermostat in target_thermostats:
|
||||
thermostat.update_ha_state(True)
|
||||
|
||||
hass.services.register(
|
||||
DOMAIN, SERVICE_SET_AWAY_MODE, thermostat_service)
|
||||
descriptions = load_yaml_config_file(
|
||||
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
||||
|
||||
hass.services.register(
|
||||
DOMAIN, SERVICE_SET_TEMPERATURE, thermostat_service)
|
||||
DOMAIN, SERVICE_SET_AWAY_MODE, thermostat_service,
|
||||
descriptions.get(SERVICE_SET_AWAY_MODE))
|
||||
|
||||
hass.services.register(
|
||||
DOMAIN, SERVICE_SET_TEMPERATURE, thermostat_service,
|
||||
descriptions.get(SERVICE_SET_TEMPERATURE))
|
||||
|
||||
return True
|
||||
|
||||
|
@ -129,22 +136,16 @@ class ThermostatDevice(Entity):
|
|||
def state_attributes(self):
|
||||
""" Returns optional state attributes. """
|
||||
|
||||
thermostat_unit = self.unit_of_measurement
|
||||
user_unit = self.hass.config.temperature_unit
|
||||
|
||||
data = {
|
||||
ATTR_CURRENT_TEMPERATURE: round(convert(
|
||||
self.current_temperature, thermostat_unit, user_unit), 1),
|
||||
ATTR_MIN_TEMP: round(convert(
|
||||
self.min_temp, thermostat_unit, user_unit), 0),
|
||||
ATTR_MAX_TEMP: round(convert(
|
||||
self.max_temp, thermostat_unit, user_unit), 0),
|
||||
ATTR_TEMPERATURE: round(convert(
|
||||
self.target_temperature, thermostat_unit, user_unit), 0),
|
||||
ATTR_TEMPERATURE_LOW: round(convert(
|
||||
self.target_temperature_low, thermostat_unit, user_unit), 0),
|
||||
ATTR_TEMPERATURE_HIGH: round(convert(
|
||||
self.target_temperature_high, thermostat_unit, user_unit), 0),
|
||||
ATTR_CURRENT_TEMPERATURE:
|
||||
self._convert(self.current_temperature, 1),
|
||||
ATTR_MIN_TEMP: self._convert(self.min_temp, 0),
|
||||
ATTR_MAX_TEMP: self._convert(self.max_temp, 0),
|
||||
ATTR_TEMPERATURE: self._convert(self.target_temperature, 0),
|
||||
ATTR_TEMPERATURE_LOW:
|
||||
self._convert(self.target_temperature_low, 0),
|
||||
ATTR_TEMPERATURE_HIGH:
|
||||
self._convert(self.target_temperature_high, 0),
|
||||
}
|
||||
|
||||
operation = self.operation
|
||||
|
@ -221,3 +222,14 @@ class ThermostatDevice(Entity):
|
|||
def max_temp(self):
|
||||
""" Return maxmum temperature. """
|
||||
return convert(35, TEMP_CELCIUS, self.unit_of_measurement)
|
||||
|
||||
def _convert(self, temp, round_dec=None):
|
||||
""" Convert temperature from this thermost into user preferred
|
||||
temperature. """
|
||||
if temp is None:
|
||||
return None
|
||||
|
||||
value = convert(temp, self.unit_of_measurement,
|
||||
self.hass.config.temperature_unit)
|
||||
|
||||
return value if round_dec is None else round(value, round_dec)
|
||||
|
|
|
@ -190,6 +190,13 @@ class HeatControl(ThermostatDevice):
|
|||
if self._heater_manual_changed:
|
||||
self.set_temperature(None)
|
||||
|
||||
@property
|
||||
def is_away_mode_on(self):
|
||||
"""
|
||||
Returns if away mode is on.
|
||||
"""
|
||||
return self._away
|
||||
|
||||
def turn_away_mode_on(self):
|
||||
""" Turns away mode on. """
|
||||
self._away = True
|
||||
|
|
0
homeassistant/components/thermostat/services.yaml
Normal file
0
homeassistant/components/thermostat/services.yaml
Normal file
152
homeassistant/components/zone.py
Normal file
152
homeassistant/components/zone.py
Normal file
|
@ -0,0 +1,152 @@
|
|||
"""
|
||||
homeassistant.components.zone
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Allows defintion of zones in Home Assistant.
|
||||
|
||||
zone:
|
||||
name: School
|
||||
latitude: 32.8773367
|
||||
longitude: -117.2494053
|
||||
# Optional radius in meters (default: 100)
|
||||
radius: 250
|
||||
# Optional icon to show instead of name
|
||||
# See https://www.google.com/design/icons/
|
||||
# Example: home, work, group-work, shopping-cart, social:people
|
||||
icon: group-work
|
||||
|
||||
zone 2:
|
||||
name: Work
|
||||
latitude: 32.8753367
|
||||
longitude: -117.2474053
|
||||
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.const import (
|
||||
ATTR_HIDDEN, ATTR_LATITUDE, ATTR_LONGITUDE, CONF_NAME)
|
||||
from homeassistant.helpers import extract_domain_configs, generate_entity_id
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.util.location import distance
|
||||
|
||||
DOMAIN = "zone"
|
||||
DEPENDENCIES = []
|
||||
ENTITY_ID_FORMAT = 'zone.{}'
|
||||
ENTITY_ID_HOME = ENTITY_ID_FORMAT.format('home')
|
||||
STATE = 'zoning'
|
||||
|
||||
DEFAULT_NAME = 'Unnamed zone'
|
||||
|
||||
ATTR_RADIUS = 'radius'
|
||||
DEFAULT_RADIUS = 100
|
||||
|
||||
ATTR_ICON = 'icon'
|
||||
ICON_HOME = 'home'
|
||||
|
||||
|
||||
def active_zone(hass, latitude, longitude, radius=0):
|
||||
""" Find the active zone for given latitude, longitude. """
|
||||
# Sort entity IDs so that we are deterministic if equal distance to 2 zones
|
||||
zones = (hass.states.get(entity_id) for entity_id
|
||||
in sorted(hass.states.entity_ids(DOMAIN)))
|
||||
|
||||
min_dist = None
|
||||
closest = None
|
||||
|
||||
for zone in zones:
|
||||
zone_dist = distance(
|
||||
latitude, longitude,
|
||||
zone.attributes[ATTR_LATITUDE], zone.attributes[ATTR_LONGITUDE])
|
||||
|
||||
within_zone = zone_dist - radius < zone.attributes[ATTR_RADIUS]
|
||||
closer_zone = closest is None or zone_dist < min_dist
|
||||
smaller_zone = (zone_dist == min_dist and
|
||||
zone.attributes[ATTR_RADIUS] <
|
||||
closest.attributes[ATTR_RADIUS])
|
||||
|
||||
if within_zone and (closer_zone or smaller_zone):
|
||||
min_dist = zone_dist
|
||||
closest = zone
|
||||
|
||||
return closest
|
||||
|
||||
|
||||
def in_zone(zone, latitude, longitude, radius=0):
|
||||
""" Test if given latitude, longitude is in given zone. """
|
||||
zone_dist = distance(
|
||||
latitude, longitude,
|
||||
zone.attributes[ATTR_LATITUDE], zone.attributes[ATTR_LONGITUDE])
|
||||
|
||||
return zone_dist - radius < zone.attributes[ATTR_RADIUS]
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
""" Setup zone. """
|
||||
entities = set()
|
||||
|
||||
for key in extract_domain_configs(config, DOMAIN):
|
||||
entries = config[key]
|
||||
if not isinstance(entries, list):
|
||||
entries = entries,
|
||||
|
||||
for entry in entries:
|
||||
name = entry.get(CONF_NAME, DEFAULT_NAME)
|
||||
latitude = entry.get(ATTR_LATITUDE)
|
||||
longitude = entry.get(ATTR_LONGITUDE)
|
||||
radius = entry.get(ATTR_RADIUS, DEFAULT_RADIUS)
|
||||
icon = entry.get(ATTR_ICON)
|
||||
|
||||
if None in (latitude, longitude):
|
||||
logging.getLogger(__name__).error(
|
||||
'Each zone needs a latitude and longitude.')
|
||||
continue
|
||||
|
||||
zone = Zone(hass, name, latitude, longitude, radius, icon)
|
||||
zone.entity_id = generate_entity_id(ENTITY_ID_FORMAT, name,
|
||||
entities)
|
||||
zone.update_ha_state()
|
||||
entities.add(zone.entity_id)
|
||||
|
||||
if ENTITY_ID_HOME not in entities:
|
||||
zone = Zone(hass, hass.config.location_name, hass.config.latitude,
|
||||
hass.config.longitude, DEFAULT_RADIUS, ICON_HOME)
|
||||
zone.entity_id = ENTITY_ID_HOME
|
||||
zone.update_ha_state()
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class Zone(Entity):
|
||||
""" Represents a Zone in Home Assistant. """
|
||||
# pylint: disable=too-many-arguments
|
||||
def __init__(self, hass, name, latitude, longitude, radius, icon):
|
||||
self.hass = hass
|
||||
self._name = name
|
||||
self.latitude = latitude
|
||||
self.longitude = longitude
|
||||
self.radius = radius
|
||||
self.icon = icon
|
||||
|
||||
def should_poll(self):
|
||||
return False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
""" The state property really does nothing for a zone. """
|
||||
return STATE
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
attr = {
|
||||
ATTR_HIDDEN: True,
|
||||
ATTR_LATITUDE: self.latitude,
|
||||
ATTR_LONGITUDE: self.longitude,
|
||||
ATTR_RADIUS: self.radius,
|
||||
}
|
||||
if self.icon:
|
||||
attr[ATTR_ICON] = self.icon
|
||||
return attr
|
|
@ -1,6 +1,7 @@
|
|||
# coding: utf-8
|
||||
""" Constants used by Home Assistant components. """
|
||||
|
||||
__version__ = "0.7.4dev0"
|
||||
__version__ = "0.7.6.dev0"
|
||||
|
||||
# Can be used to specify a catch all when registering state or event listeners.
|
||||
MATCH_ALL = '*'
|
||||
|
@ -100,6 +101,13 @@ ATTR_LAST_TRIP_TIME = "last_tripped_time"
|
|||
# For all entity's, this hold whether or not it should be hidden
|
||||
ATTR_HIDDEN = "hidden"
|
||||
|
||||
# Location of the entity
|
||||
ATTR_LATITUDE = "latitude"
|
||||
ATTR_LONGITUDE = "longitude"
|
||||
|
||||
# Accuracy of location in meters
|
||||
ATTR_GPS_ACCURACY = 'gps_accuracy'
|
||||
|
||||
# #### SERVICES ####
|
||||
SERVICE_HOMEASSISTANT_STOP = "stop"
|
||||
|
||||
|
|
|
@ -446,9 +446,8 @@ class StateMachine(object):
|
|||
|
||||
domain_filter = domain_filter.lower()
|
||||
|
||||
return [state.entity_id for key, state
|
||||
in self._states.items()
|
||||
if util.split_entity_id(key)[0] == domain_filter]
|
||||
return [state.entity_id for state in self._states.values()
|
||||
if state.domain == domain_filter]
|
||||
|
||||
def all(self):
|
||||
""" Returns a list of all states. """
|
||||
|
@ -525,6 +524,28 @@ class StateMachine(object):
|
|||
from_state, to_state)
|
||||
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
class Service(object):
|
||||
""" Represents a service. """
|
||||
|
||||
__slots__ = ['func', 'description', 'fields']
|
||||
|
||||
def __init__(self, func, description, fields):
|
||||
self.func = func
|
||||
self.description = description or ''
|
||||
self.fields = fields or {}
|
||||
|
||||
def as_dict(self):
|
||||
""" Return dictionary representation of this service. """
|
||||
return {
|
||||
'description': self.description,
|
||||
'fields': self.fields,
|
||||
}
|
||||
|
||||
def __call__(self, call):
|
||||
self.func(call)
|
||||
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
class ServiceCall(object):
|
||||
""" Represents a call to a service. """
|
||||
|
@ -559,20 +580,29 @@ class ServiceRegistry(object):
|
|||
def services(self):
|
||||
""" Dict with per domain a list of available services. """
|
||||
with self._lock:
|
||||
return {domain: list(self._services[domain].keys())
|
||||
return {domain: {key: value.as_dict() for key, value
|
||||
in self._services[domain].items()}
|
||||
for domain in self._services}
|
||||
|
||||
def has_service(self, domain, service):
|
||||
""" Returns True if specified service exists. """
|
||||
return service in self._services.get(domain, [])
|
||||
|
||||
def register(self, domain, service, service_func):
|
||||
""" Register a service. """
|
||||
def register(self, domain, service, service_func, description=None):
|
||||
"""
|
||||
Register a service.
|
||||
|
||||
Description is a dict containing key 'description' to describe
|
||||
the service and a key 'fields' to describe the fields.
|
||||
"""
|
||||
description = description or {}
|
||||
service_obj = Service(service_func, description.get('description'),
|
||||
description.get('fields', {}))
|
||||
with self._lock:
|
||||
if domain in self._services:
|
||||
self._services[domain][service] = service_func
|
||||
self._services[domain][service] = service_obj
|
||||
else:
|
||||
self._services[domain] = {service: service_func}
|
||||
self._services[domain] = {service: service_obj}
|
||||
|
||||
self._bus.fire(
|
||||
EVENT_SERVICE_REGISTERED,
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
"""
|
||||
Helper methods for components within Home Assistant.
|
||||
"""
|
||||
import re
|
||||
|
||||
from homeassistant.loader import get_component
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, CONF_PLATFORM, DEVICE_DEFAULT_NAME)
|
||||
|
@ -73,7 +75,7 @@ def config_per_platform(config, domain, logger):
|
|||
config_key = domain
|
||||
found = 1
|
||||
|
||||
while config_key in config:
|
||||
for config_key in extract_domain_configs(config, domain):
|
||||
platform_config = config[config_key]
|
||||
if not isinstance(platform_config, list):
|
||||
platform_config = [platform_config]
|
||||
|
@ -89,3 +91,9 @@ def config_per_platform(config, domain, logger):
|
|||
|
||||
found += 1
|
||||
config_key = "{} {}".format(domain, found)
|
||||
|
||||
|
||||
def extract_domain_configs(config, domain):
|
||||
""" Extract keys from config for given domain name. """
|
||||
pattern = re.compile(r'^{}(| .+)$'.format(domain))
|
||||
return (key for key in config.keys() if pattern.match(key))
|
||||
|
|
|
@ -9,7 +9,11 @@ import logging
|
|||
from homeassistant.core import State
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.const import (
|
||||
STATE_ON, STATE_OFF, SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID)
|
||||
STATE_ON, STATE_OFF, SERVICE_TURN_ON, SERVICE_TURN_OFF,
|
||||
SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PAUSE,
|
||||
STATE_PLAYING, STATE_PAUSED, ATTR_ENTITY_ID)
|
||||
|
||||
from homeassistant.components.media_player import (SERVICE_PLAY_MEDIA)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -55,7 +59,15 @@ def reproduce_state(hass, states, blocking=False):
|
|||
state.entity_id)
|
||||
continue
|
||||
|
||||
if state.state == STATE_ON:
|
||||
if state.domain == 'media_player' and state.attributes and \
|
||||
'media_type' in state.attributes and \
|
||||
'media_id' in state.attributes:
|
||||
service = SERVICE_PLAY_MEDIA
|
||||
elif state.domain == 'media_player' and state.state == STATE_PAUSED:
|
||||
service = SERVICE_MEDIA_PAUSE
|
||||
elif state.domain == 'media_player' and state.state == STATE_PLAYING:
|
||||
service = SERVICE_MEDIA_PLAY
|
||||
elif state.state == STATE_ON:
|
||||
service = SERVICE_TURN_ON
|
||||
elif state.state == STATE_OFF:
|
||||
service = SERVICE_TURN_OFF
|
||||
|
|
|
@ -233,35 +233,55 @@ class Throttle(object):
|
|||
self.limit_no_throttle = limit_no_throttle
|
||||
|
||||
def __call__(self, method):
|
||||
lock = threading.Lock()
|
||||
|
||||
if self.limit_no_throttle is not None:
|
||||
method = Throttle(self.limit_no_throttle)(method)
|
||||
|
||||
# Different methods that can be passed in:
|
||||
# - a function
|
||||
# - an unbound function on a class
|
||||
# - a method (bound function on a class)
|
||||
|
||||
# We want to be able to differentiate between function and unbound
|
||||
# methods (which are considered functions).
|
||||
# All methods have the classname in their qualname seperated by a '.'
|
||||
# Functions have a '.' in their qualname if defined inline, but will
|
||||
# be prefixed by '.<locals>.' so we strip that out.
|
||||
is_func = (not hasattr(method, '__self__') and
|
||||
'.' not in method.__qualname__.split('.<locals>.')[-1])
|
||||
|
||||
@wraps(method)
|
||||
def wrapper(*args, **kwargs):
|
||||
"""
|
||||
Wrapper that allows wrapped to be called only once per min_time.
|
||||
If we cannot acquire the lock, it is running so return None.
|
||||
"""
|
||||
if not lock.acquire(False):
|
||||
# pylint: disable=protected-access
|
||||
if hasattr(method, '__self__'):
|
||||
host = method.__self__
|
||||
elif is_func:
|
||||
host = wrapper
|
||||
else:
|
||||
host = args[0] if args else wrapper
|
||||
|
||||
if not hasattr(host, '_throttle_lock'):
|
||||
host._throttle_lock = threading.Lock()
|
||||
|
||||
if not host._throttle_lock.acquire(False):
|
||||
return None
|
||||
|
||||
last_call = getattr(host, '_throttle_last_call', None)
|
||||
# Check if method is never called or no_throttle is given
|
||||
force = not last_call or kwargs.pop('no_throttle', False)
|
||||
|
||||
try:
|
||||
last_call = wrapper.last_call
|
||||
|
||||
# Check if method is never called or no_throttle is given
|
||||
force = not last_call or kwargs.pop('no_throttle', False)
|
||||
|
||||
if force or utcnow() - last_call > self.min_time:
|
||||
result = method(*args, **kwargs)
|
||||
wrapper.last_call = utcnow()
|
||||
host._throttle_last_call = utcnow()
|
||||
return result
|
||||
else:
|
||||
return None
|
||||
finally:
|
||||
lock.release()
|
||||
|
||||
wrapper.last_call = None
|
||||
host._throttle_lock.release()
|
||||
|
||||
return wrapper
|
||||
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
"""Module with location helpers."""
|
||||
import collections
|
||||
from math import radians, cos, sin, asin, sqrt
|
||||
|
||||
import requests
|
||||
from vincenty import vincenty
|
||||
|
||||
|
||||
LocationInfo = collections.namedtuple(
|
||||
|
@ -31,18 +31,6 @@ def detect_location_info():
|
|||
return LocationInfo(**data)
|
||||
|
||||
|
||||
# From: http://stackoverflow.com/a/4913653/646416
|
||||
def distance(lon1, lat1, lon2, lat2):
|
||||
"""
|
||||
Calculate the great circle distance in meters between two points specified
|
||||
in decimal degrees on the earth using the Haversine algorithm.
|
||||
"""
|
||||
# convert decimal degrees to radians
|
||||
lon1, lat1, lon2, lat2 = (radians(val) for val in (lon1, lat1, lon2, lat2))
|
||||
|
||||
dlon = lon2 - lon1
|
||||
dlat = lat2 - lat1
|
||||
angle = sin(dlat/2)**2 + cos(lat1) * cos(lat2) * sin(dlon/2)**2
|
||||
# Radius of earth in meters.
|
||||
radius = 6371000
|
||||
return 2 * radius * asin(sqrt(angle))
|
||||
def distance(lat1, lon1, lat2, lon2):
|
||||
""" Calculate the distance in meters between two points. """
|
||||
return vincenty((lat1, lon1), (lat2, lon2)) * 1000
|
||||
|
|
|
@ -3,93 +3,94 @@ requests>=2,<3
|
|||
pyyaml>=3.11,<4
|
||||
pytz>=2015.4
|
||||
pip>=7.0.0
|
||||
vincenty==0.1.2
|
||||
|
||||
# Optional, needed for specific components
|
||||
|
||||
# Sun (sun)
|
||||
astral==0.8.1
|
||||
|
||||
# Philips Hue library (lights.hue)
|
||||
# Philips Hue (lights.hue)
|
||||
phue==0.8
|
||||
|
||||
# Limitlessled/Easybulb/Milight library (lights.limitlessled)
|
||||
ledcontroller==1.0.7
|
||||
# Limitlessled/Easybulb/Milight (lights.limitlessled)
|
||||
ledcontroller==1.1.0
|
||||
|
||||
# Chromecast bindings (media_player.cast)
|
||||
# Chromecast (media_player.cast)
|
||||
pychromecast==0.6.12
|
||||
|
||||
# Keyboard (keyboard)
|
||||
pyuserinput==0.1.9
|
||||
|
||||
# Tellstick bindings (*.tellstick)
|
||||
tellcore-py==1.0.4
|
||||
# Tellstick (*.tellstick)
|
||||
tellcore-py==1.1.2
|
||||
|
||||
# Nmap bindings (device_tracker.nmap)
|
||||
# Nmap (device_tracker.nmap)
|
||||
python-nmap==0.4.3
|
||||
|
||||
# PushBullet bindings (notify.pushbullet)
|
||||
# PushBullet (notify.pushbullet)
|
||||
pushbullet.py==0.7.1
|
||||
|
||||
# Nest Thermostat bindings (thermostat.nest)
|
||||
# Nest Thermostat (thermostat.nest)
|
||||
python-nest==2.6.0
|
||||
|
||||
# Z-Wave (*.zwave)
|
||||
pydispatcher==2.0.5
|
||||
|
||||
# ISY994 bindings (*.isy994)
|
||||
# ISY994 (isy994)
|
||||
PyISY==1.0.5
|
||||
|
||||
# PSutil (sensor.systemmonitor)
|
||||
psutil==3.0.0
|
||||
|
||||
# Pushover bindings (notify.pushover)
|
||||
# Pushover (notify.pushover)
|
||||
python-pushover==0.2
|
||||
|
||||
# Transmission Torrent Client (*.transmission)
|
||||
transmissionrpc==0.11
|
||||
|
||||
# OpenWeatherMap Web API (sensor.openweathermap)
|
||||
# OpenWeatherMap (sensor.openweathermap)
|
||||
pyowm==2.2.1
|
||||
|
||||
# XMPP Bindings (notify.xmpp)
|
||||
# XMPP (notify.xmpp)
|
||||
sleekxmpp==1.3.1
|
||||
dnspython3==1.12.0
|
||||
|
||||
# Blockchain (sensor.bitcoin)
|
||||
blockchain==1.1.2
|
||||
|
||||
# MPD Bindings (media_player.mpd)
|
||||
# Music Player Daemon (media_player.mpd)
|
||||
python-mpd2==0.5.4
|
||||
|
||||
# Hikvision (switch.hikvisioncam)
|
||||
hikvision==0.4
|
||||
|
||||
# console log coloring
|
||||
# Console log coloring
|
||||
colorlog==2.6.0
|
||||
|
||||
# JSON-RPC interface (media_player.kodi)
|
||||
jsonrpc-requests==0.1
|
||||
|
||||
# Forecast.io Bindings (sensor.forecast)
|
||||
# Forecast.io (sensor.forecast)
|
||||
python-forecastio==1.3.3
|
||||
|
||||
# Firmata Bindings (*.arduino)
|
||||
# Firmata (*.arduino)
|
||||
PyMata==2.07a
|
||||
|
||||
# Rfxtrx sensor (sensor.rfxtrx)
|
||||
# Rfxtrx (rfxtrx)
|
||||
https://github.com/Danielhiversen/pyRFXtrx/archive/ec7a1aaddf8270db6e5da1c13d58c1547effd7cf.zip#RFXtrx==0.15
|
||||
|
||||
# Mysensors
|
||||
https://github.com/theolind/pymysensors/archive/35b87d880147a34107da0d40cb815d75e6cb4af7.zip#pymysensors==0.2
|
||||
# Mysensors (sensor.mysensors)
|
||||
https://github.com/theolind/pymysensors/archive/d4b809c2167650691058d1e29bfd2c4b1792b4b0.zip#pymysensors==0.3
|
||||
|
||||
# Netgear (device_tracker.netgear)
|
||||
pynetgear==0.3
|
||||
|
||||
# Netdisco (discovery)
|
||||
netdisco==0.4.1
|
||||
netdisco==0.4.2
|
||||
|
||||
# Wemo (switch.wemo)
|
||||
pywemo==0.3
|
||||
pywemo==0.3.1
|
||||
|
||||
# Wink (*.wink)
|
||||
https://github.com/balloob/python-wink/archive/c2b700e8ca866159566ecf5e644d9c297f69f257.zip#python-wink==0.1
|
||||
|
@ -100,18 +101,18 @@ slacker==0.6.8
|
|||
# Temper sensors (sensor.temper)
|
||||
https://github.com/rkabadi/temper-python/archive/3dbdaf2d87b8db9a3cd6e5585fc704537dd2d09b.zip#temperusb==1.2.3
|
||||
|
||||
# PyEdimax
|
||||
# PyEdimax (switch.edimax)
|
||||
https://github.com/rkabadi/pyedimax/archive/365301ce3ff26129a7910c501ead09ea625f3700.zip#pyedimax==0.1
|
||||
|
||||
# RPI-GPIO platform (*.rpi_gpio)
|
||||
# Uncomment for Raspberry Pi
|
||||
# RPi.GPIO==0.5.11
|
||||
|
||||
# Adafruit temperature/humidity sensor
|
||||
# uncomment on a Raspberry Pi / Beaglebone
|
||||
# Adafruit temperature/humidity sensor (sensor.dht)
|
||||
# Uncomment on a Raspberry Pi / Beaglebone
|
||||
# http://github.com/mala-zaba/Adafruit_Python_DHT/archive/4101340de8d2457dd194bca1e8d11cbfc237e919.zip#Adafruit_DHT==1.1.0
|
||||
|
||||
# PAHO MQTT Binding (mqtt)
|
||||
# PAHO MQTT (mqtt)
|
||||
paho-mqtt==1.1
|
||||
|
||||
# PyModbus (modbus)
|
||||
|
@ -120,19 +121,26 @@ https://github.com/bashwork/pymodbus/archive/d7fc4f1cc975631e0a9011390e8017f64b6
|
|||
# Verisure (verisure)
|
||||
https://github.com/persandstrom/python-verisure/archive/9873c4527f01b1ba1f72ae60f7f35854390d59be.zip#python-verisure==0.2.6
|
||||
|
||||
# Python tools for interacting with IFTTT Maker Channel (ifttt)
|
||||
# IFTTT Maker Channel (ifttt)
|
||||
pyfttt==0.3
|
||||
|
||||
# sensor.sabnzbd
|
||||
# SABnzbd (sensor.sabnzbd)
|
||||
https://github.com/balloob/home-assistant-nzb-clients/archive/616cad59154092599278661af17e2a9f2cf5e2a9.zip#python-sabnzbd==0.1
|
||||
|
||||
# switch.vera
|
||||
# sensor.vera
|
||||
# light.vera
|
||||
# Vera (*.vera)
|
||||
https://github.com/balloob/home-assistant-vera-api/archive/a8f823066ead6c7da6fb5e7abaf16fef62e63364.zip#python-vera==0.1
|
||||
|
||||
# Sonos bindings (media_player.sonos)
|
||||
# Sonos (media_player.sonos)
|
||||
SoCo==0.11.1
|
||||
|
||||
# PlexAPI (media_player.plex)
|
||||
https://github.com/miniconfig/python-plexapi/archive/437e36dca3b7780dc0cb73941d662302c0cd2fa9.zip#python-plexapi==1.0.2
|
||||
https://github.com/adrienbrault/python-plexapi/archive/df2d0847e801d6d5cda920326d693cf75f304f1a.zip#python-plexapi==1.0.2
|
||||
|
||||
# SNMP (device_tracker.snmp)
|
||||
pysnmp==4.2.5
|
||||
|
||||
# Blinkstick (light.blinksticklight)
|
||||
blinkstick==1.1.7
|
||||
|
||||
# Telegram (notify.telegram)
|
||||
python-telegram-bot==2.8.7
|
||||
|
|
14
setup.py
14
setup.py
|
@ -9,19 +9,22 @@ DOWNLOAD_URL = ('https://github.com/balloob/home-assistant/archive/'
|
|||
|
||||
PACKAGES = find_packages(exclude=['tests', 'tests.*'])
|
||||
|
||||
PACKAGE_DATA = \
|
||||
{'homeassistant.components.frontend': ['index.html.template'],
|
||||
'homeassistant.components.frontend.www_static': ['*.*'],
|
||||
'homeassistant.components.frontend.www_static.images': ['*.*'],
|
||||
'homeassistant.startup': ['*.*']}
|
||||
# PACKAGE_DATA = \
|
||||
# {'homeassistant.components.frontend': ['index.html.template'],
|
||||
# 'homeassistant.components.frontend.www_static': ['*.*'],
|
||||
# 'homeassistant.components.frontend.www_static.images': ['*.*'],
|
||||
# 'homeassistant.components.mqtt': ['*.crt'],
|
||||
# 'homeassistant.startup': ['*.*']}
|
||||
|
||||
REQUIRES = [
|
||||
'requests>=2,<3',
|
||||
'pyyaml>=3.11,<4',
|
||||
'pytz>=2015.4',
|
||||
'pip>=7.0.0',
|
||||
'vincenty==0.1.2'
|
||||
]
|
||||
|
||||
# package_data=PACKAGE_DATA,
|
||||
setup(
|
||||
name=PACKAGE_NAME,
|
||||
version=__version__,
|
||||
|
@ -33,7 +36,6 @@ setup(
|
|||
description='Open-source home automation platform running on Python 3.',
|
||||
packages=PACKAGES,
|
||||
include_package_data=True,
|
||||
package_data=PACKAGE_DATA,
|
||||
zip_safe=False,
|
||||
platforms='any',
|
||||
install_requires=REQUIRES,
|
||||
|
|
|
@ -124,14 +124,17 @@ def mock_http_component(hass):
|
|||
hass.config.components.append('http')
|
||||
|
||||
|
||||
def mock_mqtt_component(hass):
|
||||
with mock.patch('homeassistant.components.mqtt.MQTT'):
|
||||
mqtt.setup(hass, {
|
||||
mqtt.DOMAIN: {
|
||||
mqtt.CONF_BROKER: 'mock-broker',
|
||||
}
|
||||
})
|
||||
hass.config.components.append(mqtt.DOMAIN)
|
||||
@mock.patch('homeassistant.components.mqtt.MQTT')
|
||||
@mock.patch('homeassistant.components.mqtt.MQTT.publish')
|
||||
def mock_mqtt_component(hass, mock_mqtt, mock_mqtt_publish):
|
||||
mqtt.setup(hass, {
|
||||
mqtt.DOMAIN: {
|
||||
mqtt.CONF_BROKER: 'mock-broker',
|
||||
}
|
||||
})
|
||||
hass.config.components.append(mqtt.DOMAIN)
|
||||
|
||||
return mock_mqtt_publish
|
||||
|
||||
|
||||
class MockHTTP(object):
|
||||
|
|
|
@ -11,7 +11,7 @@ import homeassistant.components.automation as automation
|
|||
from tests.common import mock_mqtt_component, fire_mqtt_message
|
||||
|
||||
|
||||
class TestAutomationState(unittest.TestCase):
|
||||
class TestAutomationMQTT(unittest.TestCase):
|
||||
""" Test the event automation. """
|
||||
|
||||
def setUp(self): # pylint: disable=invalid-name
|
||||
|
|
|
@ -8,6 +8,7 @@ import unittest
|
|||
|
||||
import homeassistant.core as ha
|
||||
import homeassistant.components.automation as automation
|
||||
import homeassistant.components.automation.state as state
|
||||
|
||||
|
||||
class TestAutomationState(unittest.TestCase):
|
||||
|
@ -334,3 +335,19 @@ class TestAutomationState(unittest.TestCase):
|
|||
self.hass.pool.block_till_done()
|
||||
|
||||
self.assertEqual(1, len(self.calls))
|
||||
|
||||
def test_if_fails_setup_if_to_boolean_value(self):
|
||||
self.assertFalse(state.trigger(
|
||||
self.hass, {
|
||||
'platform': 'state',
|
||||
'entity_id': 'test.entity',
|
||||
'to': True,
|
||||
}, lambda x: x))
|
||||
|
||||
def test_if_fails_setup_if_from_boolean_value(self):
|
||||
self.assertFalse(state.trigger(
|
||||
self.hass, {
|
||||
'platform': 'state',
|
||||
'entity_id': 'test.entity',
|
||||
'from': True,
|
||||
}, lambda x: x))
|
||||
|
|
181
tests/components/automation/test_zone.py
Normal file
181
tests/components/automation/test_zone.py
Normal file
|
@ -0,0 +1,181 @@
|
|||
"""
|
||||
tests.components.automation.test_location
|
||||
±±±~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Tests location automation.
|
||||
"""
|
||||
import unittest
|
||||
|
||||
from homeassistant.components import automation, zone
|
||||
|
||||
from tests.common import get_test_home_assistant
|
||||
|
||||
|
||||
class TestAutomationZone(unittest.TestCase):
|
||||
""" Test the event automation. """
|
||||
|
||||
def setUp(self): # pylint: disable=invalid-name
|
||||
self.hass = get_test_home_assistant()
|
||||
zone.setup(self.hass, {
|
||||
'zone': {
|
||||
'name': 'test',
|
||||
'latitude': 32.880837,
|
||||
'longitude': -117.237561,
|
||||
'radius': 250,
|
||||
}
|
||||
})
|
||||
|
||||
self.calls = []
|
||||
|
||||
def record_call(service):
|
||||
self.calls.append(service)
|
||||
|
||||
self.hass.services.register('test', 'automation', record_call)
|
||||
|
||||
def tearDown(self): # pylint: disable=invalid-name
|
||||
""" Stop down stuff we started. """
|
||||
self.hass.stop()
|
||||
|
||||
def test_if_fires_on_zone_enter(self):
|
||||
self.hass.states.set('test.entity', 'hello', {
|
||||
'latitude': 32.881011,
|
||||
'longitude': -117.234758
|
||||
})
|
||||
self.hass.pool.block_till_done()
|
||||
|
||||
self.assertTrue(automation.setup(self.hass, {
|
||||
automation.DOMAIN: {
|
||||
'trigger': {
|
||||
'platform': 'zone',
|
||||
'entity_id': 'test.entity',
|
||||
'zone': 'zone.test',
|
||||
'event': 'enter',
|
||||
},
|
||||
'action': {
|
||||
'service': 'test.automation',
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
self.hass.states.set('test.entity', 'hello', {
|
||||
'latitude': 32.880586,
|
||||
'longitude': -117.237564
|
||||
})
|
||||
self.hass.pool.block_till_done()
|
||||
|
||||
self.assertEqual(1, len(self.calls))
|
||||
|
||||
def test_if_not_fires_for_enter_on_zone_leave(self):
|
||||
self.hass.states.set('test.entity', 'hello', {
|
||||
'latitude': 32.880586,
|
||||
'longitude': -117.237564
|
||||
})
|
||||
self.hass.pool.block_till_done()
|
||||
|
||||
self.assertTrue(automation.setup(self.hass, {
|
||||
automation.DOMAIN: {
|
||||
'trigger': {
|
||||
'platform': 'zone',
|
||||
'entity_id': 'test.entity',
|
||||
'zone': 'zone.test',
|
||||
'event': 'enter',
|
||||
},
|
||||
'action': {
|
||||
'service': 'test.automation',
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
self.hass.states.set('test.entity', 'hello', {
|
||||
'latitude': 32.881011,
|
||||
'longitude': -117.234758
|
||||
})
|
||||
self.hass.pool.block_till_done()
|
||||
|
||||
self.assertEqual(0, len(self.calls))
|
||||
|
||||
def test_if_fires_on_zone_leave(self):
|
||||
self.hass.states.set('test.entity', 'hello', {
|
||||
'latitude': 32.880586,
|
||||
'longitude': -117.237564
|
||||
})
|
||||
self.hass.pool.block_till_done()
|
||||
|
||||
self.assertTrue(automation.setup(self.hass, {
|
||||
automation.DOMAIN: {
|
||||
'trigger': {
|
||||
'platform': 'zone',
|
||||
'entity_id': 'test.entity',
|
||||
'zone': 'zone.test',
|
||||
'event': 'leave',
|
||||
},
|
||||
'action': {
|
||||
'service': 'test.automation',
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
self.hass.states.set('test.entity', 'hello', {
|
||||
'latitude': 32.881011,
|
||||
'longitude': -117.234758
|
||||
})
|
||||
self.hass.pool.block_till_done()
|
||||
|
||||
self.assertEqual(1, len(self.calls))
|
||||
|
||||
def test_if_not_fires_for_leave_on_zone_enter(self):
|
||||
self.hass.states.set('test.entity', 'hello', {
|
||||
'latitude': 32.881011,
|
||||
'longitude': -117.234758
|
||||
})
|
||||
self.hass.pool.block_till_done()
|
||||
|
||||
self.assertTrue(automation.setup(self.hass, {
|
||||
automation.DOMAIN: {
|
||||
'trigger': {
|
||||
'platform': 'zone',
|
||||
'entity_id': 'test.entity',
|
||||
'zone': 'zone.test',
|
||||
'event': 'leave',
|
||||
},
|
||||
'action': {
|
||||
'service': 'test.automation',
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
self.hass.states.set('test.entity', 'hello', {
|
||||
'latitude': 32.880586,
|
||||
'longitude': -117.237564
|
||||
})
|
||||
self.hass.pool.block_till_done()
|
||||
|
||||
self.assertEqual(0, len(self.calls))
|
||||
|
||||
def test_zone_condition(self):
|
||||
self.hass.states.set('test.entity', 'hello', {
|
||||
'latitude': 32.880586,
|
||||
'longitude': -117.237564
|
||||
})
|
||||
self.hass.pool.block_till_done()
|
||||
|
||||
self.assertTrue(automation.setup(self.hass, {
|
||||
automation.DOMAIN: {
|
||||
'trigger': {
|
||||
'platform': 'event',
|
||||
'event_type': 'test_event'
|
||||
},
|
||||
'condition': {
|
||||
'platform': 'zone',
|
||||
'entity_id': 'test.entity',
|
||||
'zone': 'zone.test',
|
||||
},
|
||||
'action': {
|
||||
'service': 'test.automation',
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
self.hass.bus.fire('test_event')
|
||||
self.hass.pool.block_till_done()
|
||||
self.assertEqual(1, len(self.calls))
|
0
tests/components/sensor/__init__.py
Normal file
0
tests/components/sensor/__init__.py
Normal file
41
tests/components/sensor/test_mqtt.py
Normal file
41
tests/components/sensor/test_mqtt.py
Normal file
|
@ -0,0 +1,41 @@
|
|||
"""
|
||||
tests.components.sensor.test_mqtt
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Tests mqtt sensor.
|
||||
"""
|
||||
import unittest
|
||||
|
||||
import homeassistant.core as ha
|
||||
import homeassistant.components.sensor as sensor
|
||||
from tests.common import mock_mqtt_component, fire_mqtt_message
|
||||
|
||||
|
||||
class TestSensorMQTT(unittest.TestCase):
|
||||
""" Test the MQTT sensor. """
|
||||
|
||||
def setUp(self): # pylint: disable=invalid-name
|
||||
self.hass = ha.HomeAssistant()
|
||||
mock_mqtt_component(self.hass)
|
||||
|
||||
def tearDown(self): # pylint: disable=invalid-name
|
||||
""" Stop down stuff we started. """
|
||||
self.hass.stop()
|
||||
|
||||
def test_setting_sensor_value_via_mqtt_message(self):
|
||||
self.assertTrue(sensor.setup(self.hass, {
|
||||
'sensor': {
|
||||
'platform': 'mqtt',
|
||||
'name': 'test',
|
||||
'state_topic': 'test-topic',
|
||||
'unit_of_measurement': 'fav unit'
|
||||
}
|
||||
}))
|
||||
|
||||
fire_mqtt_message(self.hass, 'test-topic', '100')
|
||||
self.hass.pool.block_till_done()
|
||||
state = self.hass.states.get('sensor.test')
|
||||
|
||||
self.assertEqual('100', state.state)
|
||||
self.assertEqual('fav unit',
|
||||
state.attributes.get('unit_of_measurement'))
|
0
tests/components/switch/__init__.py
Normal file
0
tests/components/switch/__init__.py
Normal file
82
tests/components/switch/test_mqtt.py
Normal file
82
tests/components/switch/test_mqtt.py
Normal file
|
@ -0,0 +1,82 @@
|
|||
"""
|
||||
tests.components.switch.test_mqtt
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Tests mqtt switch.
|
||||
"""
|
||||
import unittest
|
||||
|
||||
from homeassistant.const import STATE_ON, STATE_OFF
|
||||
import homeassistant.core as ha
|
||||
import homeassistant.components.switch as switch
|
||||
from tests.common import mock_mqtt_component, fire_mqtt_message
|
||||
|
||||
|
||||
class TestSensorMQTT(unittest.TestCase):
|
||||
""" Test the MQTT switch. """
|
||||
|
||||
def setUp(self): # pylint: disable=invalid-name
|
||||
self.hass = ha.HomeAssistant()
|
||||
self.mock_publish = mock_mqtt_component(self.hass)
|
||||
|
||||
def tearDown(self): # pylint: disable=invalid-name
|
||||
""" Stop down stuff we started. """
|
||||
self.hass.stop()
|
||||
|
||||
def test_controlling_state_via_topic(self):
|
||||
self.assertTrue(switch.setup(self.hass, {
|
||||
'switch': {
|
||||
'platform': 'mqtt',
|
||||
'name': 'test',
|
||||
'state_topic': 'state-topic',
|
||||
'command_topic': 'command-topic',
|
||||
'payload_on': 'beer on',
|
||||
'payload_off': 'beer off'
|
||||
}
|
||||
}))
|
||||
|
||||
state = self.hass.states.get('switch.test')
|
||||
self.assertEqual(STATE_OFF, state.state)
|
||||
|
||||
fire_mqtt_message(self.hass, 'state-topic', 'beer on')
|
||||
self.hass.pool.block_till_done()
|
||||
|
||||
state = self.hass.states.get('switch.test')
|
||||
self.assertEqual(STATE_ON, state.state)
|
||||
|
||||
fire_mqtt_message(self.hass, 'state-topic', 'beer off')
|
||||
self.hass.pool.block_till_done()
|
||||
|
||||
state = self.hass.states.get('switch.test')
|
||||
self.assertEqual(STATE_OFF, state.state)
|
||||
|
||||
def test_sending_mqtt_commands_and_optimistic(self):
|
||||
self.assertTrue(switch.setup(self.hass, {
|
||||
'switch': {
|
||||
'platform': 'mqtt',
|
||||
'name': 'test',
|
||||
'command_topic': 'command-topic',
|
||||
'payload_on': 'beer on',
|
||||
'payload_off': 'beer off',
|
||||
'qos': 2
|
||||
}
|
||||
}))
|
||||
|
||||
state = self.hass.states.get('switch.test')
|
||||
self.assertEqual(STATE_OFF, state.state)
|
||||
|
||||
switch.turn_on(self.hass, 'switch.test')
|
||||
self.hass.pool.block_till_done()
|
||||
|
||||
self.assertEqual(('command-topic', 'beer on', 2),
|
||||
self.mock_publish.mock_calls[-1][1])
|
||||
state = self.hass.states.get('switch.test')
|
||||
self.assertEqual(STATE_ON, state.state)
|
||||
|
||||
switch.turn_off(self.hass, 'switch.test')
|
||||
self.hass.pool.block_till_done()
|
||||
|
||||
self.assertEqual(('command-topic', 'beer off', 2),
|
||||
self.mock_publish.mock_calls[-1][1])
|
||||
state = self.hass.states.get('switch.test')
|
||||
self.assertEqual(STATE_OFF, state.state)
|
|
@ -152,9 +152,13 @@ class TestLight(unittest.TestCase):
|
|||
data)
|
||||
|
||||
method, data = dev2.last_call('turn_on')
|
||||
self.assertEqual(
|
||||
{light.ATTR_XY_COLOR: color_util.color_RGB_to_xy(255, 255, 255)},
|
||||
data)
|
||||
self.assertEquals(
|
||||
data[light.ATTR_XY_COLOR],
|
||||
color_util.color_RGB_to_xy(255, 255, 255))
|
||||
|
||||
self.assertEquals(
|
||||
data[light.ATTR_RGB_COLOR],
|
||||
[255, 255, 255])
|
||||
|
||||
method, data = dev3.last_call('turn_on')
|
||||
self.assertEqual({light.ATTR_XY_COLOR: [.4, .6]}, data)
|
||||
|
|
|
@ -40,7 +40,7 @@ class TestMediaPlayer(unittest.TestCase):
|
|||
|
||||
def test_services(self):
|
||||
"""
|
||||
Test if the call service methods conver to correct service calls.
|
||||
Test if the call service methods convert to correct service calls.
|
||||
"""
|
||||
services = {
|
||||
SERVICE_TURN_ON: media_player.turn_on,
|
||||
|
|
71
tests/components/test_shell_command.py
Normal file
71
tests/components/test_shell_command.py
Normal file
|
@ -0,0 +1,71 @@
|
|||
"""
|
||||
tests.test_shell_command
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Tests demo component.
|
||||
"""
|
||||
import os
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
from subprocess import SubprocessError
|
||||
|
||||
from homeassistant import core
|
||||
from homeassistant.components import shell_command
|
||||
|
||||
|
||||
class TestShellCommand(unittest.TestCase):
|
||||
""" Test the demo module. """
|
||||
|
||||
def setUp(self): # pylint: disable=invalid-name
|
||||
self.hass = core.HomeAssistant()
|
||||
|
||||
def tearDown(self): # pylint: disable=invalid-name
|
||||
""" Stop down stuff we started. """
|
||||
self.hass.stop()
|
||||
|
||||
def test_executing_service(self):
|
||||
""" Test if able to call a configured service. """
|
||||
with tempfile.TemporaryDirectory() as tempdirname:
|
||||
path = os.path.join(tempdirname, 'called.txt')
|
||||
self.assertTrue(shell_command.setup(self.hass, {
|
||||
'shell_command': {
|
||||
'test_service': "touch {}".format(path)
|
||||
}
|
||||
}))
|
||||
|
||||
self.hass.services.call('shell_command', 'test_service',
|
||||
blocking=True)
|
||||
|
||||
self.assertTrue(os.path.isfile(path))
|
||||
|
||||
def test_config_not_dict(self):
|
||||
""" Test if config is not a dict. """
|
||||
self.assertFalse(shell_command.setup(self.hass, {
|
||||
'shell_command': ['some', 'weird', 'list']
|
||||
}))
|
||||
|
||||
def test_config_not_valid_service_names(self):
|
||||
""" Test if config contains invalid service names. """
|
||||
self.assertFalse(shell_command.setup(self.hass, {
|
||||
'shell_command': {
|
||||
'this is invalid because space': 'touch bla.txt'
|
||||
}}))
|
||||
|
||||
@patch('homeassistant.components.shell_command.subprocess.call',
|
||||
side_effect=SubprocessError)
|
||||
@patch('homeassistant.components.shell_command._LOGGER.error')
|
||||
def test_subprocess_raising_error(self, mock_call, mock_error):
|
||||
with tempfile.TemporaryDirectory() as tempdirname:
|
||||
path = os.path.join(tempdirname, 'called.txt')
|
||||
self.assertTrue(shell_command.setup(self.hass, {
|
||||
'shell_command': {
|
||||
'test_service': "touch {}".format(path)
|
||||
}
|
||||
}))
|
||||
|
||||
self.hass.services.call('shell_command', 'test_service',
|
||||
blocking=True)
|
||||
|
||||
self.assertFalse(os.path.isfile(path))
|
||||
self.assertEqual(1, mock_error.call_count)
|
|
@ -8,9 +8,8 @@ Tests component helpers.
|
|||
import unittest
|
||||
|
||||
import homeassistant.core as ha
|
||||
import homeassistant.loader as loader
|
||||
from homeassistant import loader, helpers
|
||||
from homeassistant.const import STATE_ON, STATE_OFF, ATTR_ENTITY_ID
|
||||
from homeassistant.helpers import extract_entity_ids
|
||||
|
||||
from tests.common import get_test_home_assistant
|
||||
|
||||
|
@ -39,10 +38,22 @@ class TestComponentsCore(unittest.TestCase):
|
|||
{ATTR_ENTITY_ID: 'light.Bowl'})
|
||||
|
||||
self.assertEqual(['light.bowl'],
|
||||
extract_entity_ids(self.hass, call))
|
||||
helpers.extract_entity_ids(self.hass, call))
|
||||
|
||||
call = ha.ServiceCall('light', 'turn_on',
|
||||
{ATTR_ENTITY_ID: 'group.test'})
|
||||
|
||||
self.assertEqual(['light.ceiling', 'light.kitchen'],
|
||||
extract_entity_ids(self.hass, call))
|
||||
helpers.extract_entity_ids(self.hass, call))
|
||||
|
||||
def test_extract_domain_configs(self):
|
||||
config = {
|
||||
'zone': None,
|
||||
'zoner': None,
|
||||
'zone ': None,
|
||||
'zone Hallo': None,
|
||||
'zone 100': None,
|
||||
}
|
||||
|
||||
self.assertEqual(set(['zone', 'zone Hallo', 'zone 100']),
|
||||
set(helpers.extract_domain_configs(config, 'zone')))
|
||||
|
|
|
@ -441,7 +441,7 @@ class TestServiceRegistry(unittest.TestCase):
|
|||
|
||||
def test_services(self):
|
||||
expected = {
|
||||
'test_domain': ['test_service']
|
||||
'test_domain': {'test_service': {'description': '', 'fields': {}}}
|
||||
}
|
||||
self.assertEqual(expected, self.services.services)
|
||||
|
||||
|
|
|
@ -218,3 +218,27 @@ class TestUtil(unittest.TestCase):
|
|||
|
||||
self.assertEqual(3, len(calls1))
|
||||
self.assertEqual(2, len(calls2))
|
||||
|
||||
def test_throttle_per_instance(self):
|
||||
""" Test that the throttle method is done per instance of a class. """
|
||||
|
||||
class Tester(object):
|
||||
@util.Throttle(timedelta(seconds=1))
|
||||
def hello(self):
|
||||
return True
|
||||
|
||||
self.assertTrue(Tester().hello())
|
||||
self.assertTrue(Tester().hello())
|
||||
|
||||
def test_throttle_on_method(self):
|
||||
""" Test that throttle works when wrapping a method. """
|
||||
|
||||
class Tester(object):
|
||||
def hello(self):
|
||||
return True
|
||||
|
||||
tester = Tester()
|
||||
throttled = util.Throttle(timedelta(seconds=1))(tester.hello)
|
||||
|
||||
self.assertTrue(throttled())
|
||||
self.assertIsNone(throttled())
|
||||
|
|
Loading…
Add table
Reference in a new issue