mirror of
https://github.com/home-assistant/core.git
synced 2025-07-22 04:37:06 +00:00
commit
7f60f1e662
@ -49,6 +49,7 @@ omit =
|
|||||||
homeassistant/components/media_player/itunes.py
|
homeassistant/components/media_player/itunes.py
|
||||||
homeassistant/components/media_player/kodi.py
|
homeassistant/components/media_player/kodi.py
|
||||||
homeassistant/components/media_player/mpd.py
|
homeassistant/components/media_player/mpd.py
|
||||||
|
homeassistant/components/media_player/plex.py
|
||||||
homeassistant/components/media_player/squeezebox.py
|
homeassistant/components/media_player/squeezebox.py
|
||||||
homeassistant/components/media_player/sonos.py
|
homeassistant/components/media_player/sonos.py
|
||||||
homeassistant/components/notify/file.py
|
homeassistant/components/notify/file.py
|
||||||
@ -69,6 +70,7 @@ omit =
|
|||||||
homeassistant/components/sensor/glances.py
|
homeassistant/components/sensor/glances.py
|
||||||
homeassistant/components/sensor/mysensors.py
|
homeassistant/components/sensor/mysensors.py
|
||||||
homeassistant/components/sensor/openweathermap.py
|
homeassistant/components/sensor/openweathermap.py
|
||||||
|
homeassistant/components/sensor/rest.py
|
||||||
homeassistant/components/sensor/rfxtrx.py
|
homeassistant/components/sensor/rfxtrx.py
|
||||||
homeassistant/components/sensor/rpi_gpio.py
|
homeassistant/components/sensor/rpi_gpio.py
|
||||||
homeassistant/components/sensor/sabnzbd.py
|
homeassistant/components/sensor/sabnzbd.py
|
||||||
@ -77,6 +79,7 @@ omit =
|
|||||||
homeassistant/components/sensor/temper.py
|
homeassistant/components/sensor/temper.py
|
||||||
homeassistant/components/sensor/time_date.py
|
homeassistant/components/sensor/time_date.py
|
||||||
homeassistant/components/sensor/transmission.py
|
homeassistant/components/sensor/transmission.py
|
||||||
|
homeassistant/components/sensor/worldclock.py
|
||||||
homeassistant/components/switch/arest.py
|
homeassistant/components/switch/arest.py
|
||||||
homeassistant/components/switch/command_switch.py
|
homeassistant/components/switch/command_switch.py
|
||||||
homeassistant/components/switch/edimax.py
|
homeassistant/components/switch/edimax.py
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
sudo: false
|
sudo: false
|
||||||
language: python
|
language: python
|
||||||
|
cache:
|
||||||
|
directories:
|
||||||
|
- $HOME/virtualenv/python3.4.2/
|
||||||
python:
|
python:
|
||||||
- "3.4"
|
- "3.4"
|
||||||
install:
|
install:
|
||||||
|
@ -18,7 +18,7 @@ For help on building your component, please see the [developer documentation](ht
|
|||||||
After you finish adding support for your device:
|
After you finish adding support for your device:
|
||||||
|
|
||||||
- Update the supported devices in the `README.md` file.
|
- Update the supported devices in the `README.md` file.
|
||||||
- Add any new dependencies to `requirements.txt`.
|
- Add any new dependencies to `requirements_all.txt`. There is no ordering right now, so just add it to the end.
|
||||||
- Update the `.coveragerc` file.
|
- Update the `.coveragerc` file.
|
||||||
- Provide some documentation for [home-assistant.io](https://home-assistant.io/). The documentation is handled in a separate [git repository](https://github.com/balloob/home-assistant.io).
|
- Provide some documentation for [home-assistant.io](https://home-assistant.io/). The documentation is handled in a separate [git repository](https://github.com/balloob/home-assistant.io).
|
||||||
- Make sure all your code passes Pylint and flake8 (PEP8 and some more) validation. To generate reports, run `pylint homeassistant > pylint.txt` and `flake8 homeassistant --exclude bower_components,external > flake8.txt`.
|
- Make sure all your code passes Pylint and flake8 (PEP8 and some more) validation. To generate reports, run `pylint homeassistant > pylint.txt` and `flake8 homeassistant --exclude bower_components,external > flake8.txt`.
|
||||||
|
@ -1 +1,2 @@
|
|||||||
recursive-exclude tests *
|
recursive-exclude tests *
|
||||||
|
recursive-include homeassistant services.yaml
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
homeassistant:
|
homeassistant:
|
||||||
# Omitted values in this section will be auto detected using freegeoip.net
|
# 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
|
latitude: 32.87336
|
||||||
longitude: 117.22743
|
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
|
# 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)
|
# Grouped states should share the same type of states (ON/OFF or HOME/NOT_HOME)
|
||||||
|
# You can also have groups within groups.
|
||||||
group:
|
group:
|
||||||
|
Home:
|
||||||
|
- group.living_room
|
||||||
|
- group.kitchen
|
||||||
living_room:
|
living_room:
|
||||||
- light.Bowl
|
- light.Bowl
|
||||||
- light.Ceiling
|
- light.Ceiling
|
||||||
- light.TV_back_light
|
- light.TV_back_light
|
||||||
|
kitchen:
|
||||||
|
- light.fan_bulb_1
|
||||||
|
- light.fan_bulb_2
|
||||||
children:
|
children:
|
||||||
- device_tracker.child_1
|
- device_tracker.child_1
|
||||||
- device_tracker.child_2
|
- device_tracker.child_2
|
||||||
@ -94,28 +103,36 @@ browser:
|
|||||||
keyboard:
|
keyboard:
|
||||||
|
|
||||||
automation:
|
automation:
|
||||||
platform: state
|
- alias: 'Rule 1 Light on in the evening'
|
||||||
alias: Sun starts shining
|
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
|
- alias: 'Rule 2 - Away Mode'
|
||||||
# Next two are optional, omit to match all
|
|
||||||
state_from: below_horizon
|
|
||||||
state_to: above_horizon
|
|
||||||
|
|
||||||
execute_service: light.turn_off
|
trigger:
|
||||||
service_entity_id: group.living_room
|
- platform: state
|
||||||
|
entity_id: group.all_devices
|
||||||
|
state: 'not_home'
|
||||||
|
|
||||||
automation 2:
|
condition: use_trigger_values
|
||||||
platform: time
|
action:
|
||||||
alias: Beer o Clock
|
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!
|
|
||||||
|
|
||||||
sensor:
|
sensor:
|
||||||
platform: systemmonitor
|
platform: systemmonitor
|
||||||
@ -135,6 +152,23 @@ sensor:
|
|||||||
- type: 'process'
|
- type: 'process'
|
||||||
arg: 'octave-cli'
|
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:
|
script:
|
||||||
# Turns on the bedroom lights and then the living room lights 1 minute later
|
# Turns on the bedroom lights and then the living room lights 1 minute later
|
||||||
wakeup:
|
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()})
|
dict, {key: value or {} for key, value in config.items()})
|
||||||
|
|
||||||
# Filter out the repeating and common config section [homeassistant]
|
# Filter out the repeating and common config section [homeassistant]
|
||||||
components = (key for key in config.keys()
|
components = set(key.split(' ')[0] for key in config.keys()
|
||||||
if ' ' not in key and key != core.DOMAIN)
|
if key != core.DOMAIN)
|
||||||
|
|
||||||
if not core_components.setup(hass, config):
|
if not core_components.setup(hass, config):
|
||||||
_LOGGER.error('Home Assistant core failed to initialize. '
|
_LOGGER.error('Home Assistant core failed to initialize. '
|
||||||
@ -297,11 +297,15 @@ def process_ha_core_config(hass, config):
|
|||||||
else:
|
else:
|
||||||
_LOGGER.error('Received invalid time zone %s', time_zone_str)
|
_LOGGER.error('Received invalid time zone %s', time_zone_str)
|
||||||
|
|
||||||
for key, attr in ((CONF_LATITUDE, 'latitude'),
|
for key, attr, typ in ((CONF_LATITUDE, 'latitude', float),
|
||||||
(CONF_LONGITUDE, 'longitude'),
|
(CONF_LONGITUDE, 'longitude', float),
|
||||||
(CONF_NAME, 'location_name')):
|
(CONF_NAME, 'location_name', str)):
|
||||||
if key in config:
|
if key in config:
|
||||||
setattr(hac, attr, config[key])
|
try:
|
||||||
|
setattr(hac, attr, typ(config[key]))
|
||||||
|
except ValueError:
|
||||||
|
_LOGGER.error('Received invalid %s value for %s: %s',
|
||||||
|
typ.__name__, key, attr)
|
||||||
|
|
||||||
set_time_zone(config.get(CONF_TIME_ZONE))
|
set_time_zone(config.get(CONF_TIME_ZONE))
|
||||||
|
|
||||||
|
@ -1,15 +1,18 @@
|
|||||||
"""
|
"""
|
||||||
homeassistant.components.alarm_control_panel
|
homeassistant.components.alarm_control_panel
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
Component to interface with a alarm control panel.
|
Component to interface with a alarm control panel.
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
from homeassistant.helpers.entity import Entity
|
import os
|
||||||
from homeassistant.helpers.entity_component import EntityComponent
|
|
||||||
from homeassistant.components import verisure
|
from homeassistant.components import verisure
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_ENTITY_ID,
|
ATTR_ENTITY_ID,
|
||||||
SERVICE_ALARM_DISARM, SERVICE_ALARM_ARM_HOME, SERVICE_ALARM_ARM_AWAY)
|
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'
|
DOMAIN = 'alarm_control_panel'
|
||||||
DEPENDENCIES = []
|
DEPENDENCIES = []
|
||||||
@ -29,9 +32,11 @@ SERVICE_TO_METHOD = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ATTR_CODE = 'code'
|
ATTR_CODE = 'code'
|
||||||
|
ATTR_CODE_FORMAT = 'code_format'
|
||||||
|
|
||||||
ATTR_TO_PROPERTY = [
|
ATTR_TO_PROPERTY = [
|
||||||
ATTR_CODE,
|
ATTR_CODE,
|
||||||
|
ATTR_CODE_FORMAT
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -57,8 +62,12 @@ def setup(hass, config):
|
|||||||
for alarm in target_alarms:
|
for alarm in target_alarms:
|
||||||
getattr(alarm, method)(code)
|
getattr(alarm, method)(code)
|
||||||
|
|
||||||
|
descriptions = load_yaml_config_file(
|
||||||
|
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
||||||
|
|
||||||
for service in SERVICE_TO_METHOD:
|
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
|
return True
|
||||||
|
|
||||||
@ -93,16 +102,31 @@ def alarm_arm_away(hass, code, entity_id=None):
|
|||||||
hass.services.call(DOMAIN, SERVICE_ALARM_ARM_AWAY, data)
|
hass.services.call(DOMAIN, SERVICE_ALARM_ARM_AWAY, data)
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=no-self-use
|
||||||
class AlarmControlPanel(Entity):
|
class AlarmControlPanel(Entity):
|
||||||
""" ABC for alarm control devices. """
|
""" 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. """
|
""" Send disarm command. """
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def alarm_arm_home(self, code):
|
def alarm_arm_home(self, code=None):
|
||||||
""" Send arm home command. """
|
""" Send arm home command. """
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def alarm_arm_away(self, code):
|
def alarm_arm_away(self, code=None):
|
||||||
""" Send arm away command. """
|
""" Send arm away command. """
|
||||||
raise NotImplementedError()
|
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
|
homeassistant.components.alarm_control_panel.verisure
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
Interfaces with Verisure alarm control panel.
|
Interfaces with Verisure alarm control panel.
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
@ -34,7 +34,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
|
|
||||||
|
|
||||||
class VerisureAlarm(alarm.AlarmControlPanel):
|
class VerisureAlarm(alarm.AlarmControlPanel):
|
||||||
""" represents a Verisure alarm status within home assistant. """
|
""" Represents a Verisure alarm status. """
|
||||||
|
|
||||||
def __init__(self, alarm_status):
|
def __init__(self, alarm_status):
|
||||||
self._id = alarm_status.id
|
self._id = alarm_status.id
|
||||||
@ -51,8 +51,13 @@ class VerisureAlarm(alarm.AlarmControlPanel):
|
|||||||
""" Returns the state of the device. """
|
""" Returns the state of the device. """
|
||||||
return self._state
|
return self._state
|
||||||
|
|
||||||
|
@property
|
||||||
|
def code_format(self):
|
||||||
|
""" Four digit code required. """
|
||||||
|
return '^\\d{4}$'
|
||||||
|
|
||||||
def update(self):
|
def update(self):
|
||||||
''' update alarm status '''
|
""" Update alarm status """
|
||||||
verisure.update()
|
verisure.update()
|
||||||
|
|
||||||
if verisure.STATUS[self._device][self._id].status == 'unarmed':
|
if verisure.STATUS[self._device][self._id].status == 'unarmed':
|
||||||
@ -66,21 +71,21 @@ class VerisureAlarm(alarm.AlarmControlPanel):
|
|||||||
'Unknown alarm state %s',
|
'Unknown alarm state %s',
|
||||||
verisure.STATUS[self._device][self._id].status)
|
verisure.STATUS[self._device][self._id].status)
|
||||||
|
|
||||||
def alarm_disarm(self, code):
|
def alarm_disarm(self, code=None):
|
||||||
""" Send disarm command. """
|
""" Send disarm command. """
|
||||||
verisure.MY_PAGES.set_alarm_status(
|
verisure.MY_PAGES.set_alarm_status(
|
||||||
code,
|
code,
|
||||||
verisure.MY_PAGES.ALARM_DISARMED)
|
verisure.MY_PAGES.ALARM_DISARMED)
|
||||||
_LOGGER.warning('disarming')
|
_LOGGER.warning('disarming')
|
||||||
|
|
||||||
def alarm_arm_home(self, code):
|
def alarm_arm_home(self, code=None):
|
||||||
""" Send arm home command. """
|
""" Send arm home command. """
|
||||||
verisure.MY_PAGES.set_alarm_status(
|
verisure.MY_PAGES.set_alarm_status(
|
||||||
code,
|
code,
|
||||||
verisure.MY_PAGES.ALARM_ARMED_HOME)
|
verisure.MY_PAGES.ALARM_ARMED_HOME)
|
||||||
_LOGGER.warning('arming home')
|
_LOGGER.warning('arming home')
|
||||||
|
|
||||||
def alarm_arm_away(self, code):
|
def alarm_arm_away(self, code=None):
|
||||||
""" Send arm away command. """
|
""" Send arm away command. """
|
||||||
verisure.MY_PAGES.set_alarm_status(
|
verisure.MY_PAGES.set_alarm_status(
|
||||||
code,
|
code,
|
||||||
|
@ -103,6 +103,10 @@ def _handle_get_api_stream(handler, path_match, data):
|
|||||||
write_lock = threading.Lock()
|
write_lock = threading.Lock()
|
||||||
block = threading.Event()
|
block = threading.Event()
|
||||||
|
|
||||||
|
restrict = data.get('restrict')
|
||||||
|
if restrict:
|
||||||
|
restrict = restrict.split(',')
|
||||||
|
|
||||||
def write_message(payload):
|
def write_message(payload):
|
||||||
""" Writes a message to the output. """
|
""" Writes a message to the output. """
|
||||||
with write_lock:
|
with write_lock:
|
||||||
@ -118,7 +122,8 @@ def _handle_get_api_stream(handler, path_match, data):
|
|||||||
""" Forwards events to the open request. """
|
""" Forwards events to the open request. """
|
||||||
nonlocal gracefully_closed
|
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
|
return
|
||||||
elif event.event_type == EVENT_HOMEASSISTANT_STOP:
|
elif event.event_type == EVENT_HOMEASSISTANT_STOP:
|
||||||
gracefully_closed = True
|
gracefully_closed = True
|
||||||
|
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
|
# Setup sun
|
||||||
if not hass.config.latitude:
|
if not hass.config.latitude:
|
||||||
hass.config.latitude = '32.87336'
|
hass.config.latitude = 32.87336
|
||||||
|
|
||||||
if not hass.config.longitude:
|
if not hass.config.longitude:
|
||||||
hass.config.longitude = '117.22743'
|
hass.config.longitude = 117.22743
|
||||||
|
|
||||||
bootstrap.setup_component(hass, 'sun')
|
bootstrap.setup_component(hass, 'sun')
|
||||||
|
|
||||||
@ -60,7 +60,7 @@ def setup(hass, config):
|
|||||||
{'camera': {
|
{'camera': {
|
||||||
'platform': 'generic',
|
'platform': 'generic',
|
||||||
'name': 'IP Camera',
|
'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
|
# Setup scripts
|
||||||
@ -108,7 +108,9 @@ def setup(hass, config):
|
|||||||
"http://graph.facebook.com/297400035/picture",
|
"http://graph.facebook.com/297400035/picture",
|
||||||
ATTR_FRIENDLY_NAME: 'Paulus'})
|
ATTR_FRIENDLY_NAME: 'Paulus'})
|
||||||
hass.states.set("device_tracker.anne_therese", "not_home",
|
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",
|
hass.states.set("group.all_devices", "home",
|
||||||
{
|
{
|
||||||
|
@ -17,7 +17,12 @@ device_tracker:
|
|||||||
|
|
||||||
# New found devices auto found
|
# New found devices auto found
|
||||||
track_new_devices: yes
|
track_new_devices: yes
|
||||||
|
|
||||||
|
# Maximum distance from home we consider people home
|
||||||
|
range_home: 100
|
||||||
"""
|
"""
|
||||||
|
# pylint: disable=too-many-instance-attributes, too-many-arguments
|
||||||
|
# pylint: disable=too-many-locals
|
||||||
import csv
|
import csv
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
import logging
|
import logging
|
||||||
@ -25,7 +30,7 @@ import os
|
|||||||
import threading
|
import threading
|
||||||
|
|
||||||
from homeassistant.bootstrap import prepare_setup_platform
|
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.config import load_yaml_config_file
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers import config_per_platform
|
from homeassistant.helpers import config_per_platform
|
||||||
@ -35,10 +40,11 @@ import homeassistant.util.dt as dt_util
|
|||||||
|
|
||||||
from homeassistant.helpers.event import track_utc_time_change
|
from homeassistant.helpers.event import track_utc_time_change
|
||||||
from homeassistant.const import (
|
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"
|
DOMAIN = "device_tracker"
|
||||||
DEPENDENCIES = []
|
DEPENDENCIES = ['zone']
|
||||||
|
|
||||||
GROUP_NAME_ALL_DEVICES = 'all devices'
|
GROUP_NAME_ALL_DEVICES = 'all devices'
|
||||||
ENTITY_ID_ALL_DEVICES = group.ENTITY_ID_FORMAT.format('all_devices')
|
ENTITY_ID_ALL_DEVICES = group.ENTITY_ID_FORMAT.format('all_devices')
|
||||||
@ -52,7 +58,7 @@ CONF_TRACK_NEW = "track_new_devices"
|
|||||||
DEFAULT_CONF_TRACK_NEW = True
|
DEFAULT_CONF_TRACK_NEW = True
|
||||||
|
|
||||||
CONF_CONSIDER_HOME = 'consider_home'
|
CONF_CONSIDER_HOME = 'consider_home'
|
||||||
DEFAULT_CONF_CONSIDER_HOME = 180 # seconds
|
DEFAULT_CONSIDER_HOME = 180 # seconds
|
||||||
|
|
||||||
CONF_SCAN_INTERVAL = "interval_seconds"
|
CONF_SCAN_INTERVAL = "interval_seconds"
|
||||||
DEFAULT_SCAN_INTERVAL = 12
|
DEFAULT_SCAN_INTERVAL = 12
|
||||||
@ -60,15 +66,17 @@ DEFAULT_SCAN_INTERVAL = 12
|
|||||||
CONF_AWAY_HIDE = 'hide_if_away'
|
CONF_AWAY_HIDE = 'hide_if_away'
|
||||||
DEFAULT_AWAY_HIDE = False
|
DEFAULT_AWAY_HIDE = False
|
||||||
|
|
||||||
|
CONF_HOME_RANGE = 'home_range'
|
||||||
|
DEFAULT_HOME_RANGE = 100
|
||||||
|
|
||||||
SERVICE_SEE = 'see'
|
SERVICE_SEE = 'see'
|
||||||
|
|
||||||
ATTR_LATITUDE = 'latitude'
|
|
||||||
ATTR_LONGITUDE = 'longitude'
|
|
||||||
ATTR_MAC = 'mac'
|
ATTR_MAC = 'mac'
|
||||||
ATTR_DEV_ID = 'dev_id'
|
ATTR_DEV_ID = 'dev_id'
|
||||||
ATTR_HOST_NAME = 'host_name'
|
ATTR_HOST_NAME = 'host_name'
|
||||||
ATTR_LOCATION_NAME = 'location_name'
|
ATTR_LOCATION_NAME = 'location_name'
|
||||||
ATTR_GPS = 'gps'
|
ATTR_GPS = 'gps'
|
||||||
|
ATTR_BATTERY = 'battery'
|
||||||
|
|
||||||
DISCOVERY_PLATFORMS = {
|
DISCOVERY_PLATFORMS = {
|
||||||
discovery.SERVICE_NETGEAR: 'netgear',
|
discovery.SERVICE_NETGEAR: 'netgear',
|
||||||
@ -86,7 +94,7 @@ def is_on(hass, entity_id=None):
|
|||||||
|
|
||||||
|
|
||||||
def see(hass, mac=None, dev_id=None, host_name=None, location_name=None,
|
def see(hass, mac=None, dev_id=None, host_name=None, location_name=None,
|
||||||
gps=None):
|
gps=None, gps_accuracy=None, battery=None):
|
||||||
""" Call service to notify you see device. """
|
""" Call service to notify you see device. """
|
||||||
data = {key: value for key, value in
|
data = {key: value for key, value in
|
||||||
((ATTR_MAC, mac),
|
((ATTR_MAC, mac),
|
||||||
@ -106,13 +114,17 @@ def setup(hass, config):
|
|||||||
os.remove(csv_path)
|
os.remove(csv_path)
|
||||||
|
|
||||||
conf = config.get(DOMAIN, {})
|
conf = config.get(DOMAIN, {})
|
||||||
consider_home = util.convert(conf.get(CONF_CONSIDER_HOME), int,
|
consider_home = timedelta(
|
||||||
DEFAULT_CONF_CONSIDER_HOME)
|
seconds=util.convert(conf.get(CONF_CONSIDER_HOME), int,
|
||||||
|
DEFAULT_CONSIDER_HOME))
|
||||||
track_new = util.convert(conf.get(CONF_TRACK_NEW), bool,
|
track_new = util.convert(conf.get(CONF_TRACK_NEW), bool,
|
||||||
DEFAULT_CONF_TRACK_NEW)
|
DEFAULT_CONF_TRACK_NEW)
|
||||||
|
home_range = util.convert(conf.get(CONF_HOME_RANGE), int,
|
||||||
|
DEFAULT_HOME_RANGE)
|
||||||
|
|
||||||
devices = load_config(yaml_path, hass, timedelta(seconds=consider_home))
|
devices = load_config(yaml_path, hass, consider_home, home_range)
|
||||||
tracker = DeviceTracker(hass, consider_home, track_new, devices)
|
tracker = DeviceTracker(hass, consider_home, track_new, home_range,
|
||||||
|
devices)
|
||||||
|
|
||||||
def setup_platform(p_type, p_config, disc_info=None):
|
def setup_platform(p_type, p_config, disc_info=None):
|
||||||
""" Setup a device tracker platform. """
|
""" Setup a device tracker platform. """
|
||||||
@ -158,22 +170,26 @@ def setup(hass, config):
|
|||||||
""" Service to see a device. """
|
""" Service to see a device. """
|
||||||
args = {key: value for key, value in call.data.items() if key in
|
args = {key: value for key, value in call.data.items() if key in
|
||||||
(ATTR_MAC, ATTR_DEV_ID, ATTR_HOST_NAME, ATTR_LOCATION_NAME,
|
(ATTR_MAC, ATTR_DEV_ID, ATTR_HOST_NAME, ATTR_LOCATION_NAME,
|
||||||
ATTR_GPS)}
|
ATTR_GPS, ATTR_GPS_ACCURACY, ATTR_BATTERY)}
|
||||||
tracker.see(**args)
|
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
|
return True
|
||||||
|
|
||||||
|
|
||||||
class DeviceTracker(object):
|
class DeviceTracker(object):
|
||||||
""" Track devices """
|
""" Track devices """
|
||||||
def __init__(self, hass, consider_home, track_new, devices):
|
def __init__(self, hass, consider_home, track_new, home_range, devices):
|
||||||
self.hass = hass
|
self.hass = hass
|
||||||
self.devices = {dev.dev_id: dev for dev in devices}
|
self.devices = {dev.dev_id: dev for dev in devices}
|
||||||
self.mac_to_dev = {dev.mac: dev for dev in devices if dev.mac}
|
self.mac_to_dev = {dev.mac: dev for dev in devices if dev.mac}
|
||||||
self.consider_home = timedelta(seconds=consider_home)
|
self.consider_home = consider_home
|
||||||
self.track_new = track_new
|
self.track_new = track_new
|
||||||
|
self.home_range = home_range
|
||||||
self.lock = threading.Lock()
|
self.lock = threading.Lock()
|
||||||
|
|
||||||
for device in devices:
|
for device in devices:
|
||||||
@ -183,7 +199,7 @@ class DeviceTracker(object):
|
|||||||
self.group = None
|
self.group = None
|
||||||
|
|
||||||
def see(self, mac=None, dev_id=None, host_name=None, location_name=None,
|
def see(self, mac=None, dev_id=None, host_name=None, location_name=None,
|
||||||
gps=None):
|
gps=None, gps_accuracy=None, battery=None):
|
||||||
""" Notify device tracker that you see a device. """
|
""" Notify device tracker that you see a device. """
|
||||||
with self.lock:
|
with self.lock:
|
||||||
if mac is None and dev_id is None:
|
if mac is None and dev_id is None:
|
||||||
@ -198,20 +214,21 @@ class DeviceTracker(object):
|
|||||||
device = self.devices.get(dev_id)
|
device = self.devices.get(dev_id)
|
||||||
|
|
||||||
if device:
|
if device:
|
||||||
device.seen(host_name, location_name, gps)
|
device.seen(host_name, location_name, gps, gps_accuracy,
|
||||||
|
battery)
|
||||||
if device.track:
|
if device.track:
|
||||||
device.update_ha_state()
|
device.update_ha_state()
|
||||||
return
|
return
|
||||||
|
|
||||||
# If no device can be found, create it
|
# If no device can be found, create it
|
||||||
device = Device(
|
device = Device(
|
||||||
self.hass, self.consider_home, self.track_new, dev_id, mac,
|
self.hass, self.consider_home, self.home_range, self.track_new,
|
||||||
(host_name or dev_id).replace('_', ' '))
|
dev_id, mac, (host_name or dev_id).replace('_', ' '))
|
||||||
self.devices[dev_id] = device
|
self.devices[dev_id] = device
|
||||||
if mac is not None:
|
if mac is not None:
|
||||||
self.mac_to_dev[mac] = device
|
self.mac_to_dev[mac] = device
|
||||||
|
|
||||||
device.seen(host_name, location_name, gps)
|
device.seen(host_name, location_name, gps, gps_accuracy, battery)
|
||||||
if device.track:
|
if device.track:
|
||||||
device.update_ha_state()
|
device.update_ha_state()
|
||||||
|
|
||||||
@ -239,19 +256,20 @@ class DeviceTracker(object):
|
|||||||
|
|
||||||
class Device(Entity):
|
class Device(Entity):
|
||||||
""" Tracked device. """
|
""" Tracked device. """
|
||||||
# pylint: disable=too-many-instance-attributes, too-many-arguments
|
|
||||||
|
|
||||||
host_name = None
|
host_name = None
|
||||||
location_name = None
|
location_name = None
|
||||||
gps = None
|
gps = None
|
||||||
|
gps_accuracy = 0
|
||||||
last_seen = None
|
last_seen = None
|
||||||
|
battery = None
|
||||||
|
|
||||||
# Track if the last update of this device was HOME
|
# Track if the last update of this device was HOME
|
||||||
last_update_home = False
|
last_update_home = False
|
||||||
_state = STATE_NOT_HOME
|
_state = STATE_NOT_HOME
|
||||||
|
|
||||||
def __init__(self, hass, consider_home, track, dev_id, mac, name=None,
|
def __init__(self, hass, consider_home, home_range, track, dev_id, mac,
|
||||||
picture=None, away_hide=False):
|
name=None, picture=None, away_hide=False):
|
||||||
self.hass = hass
|
self.hass = hass
|
||||||
self.entity_id = ENTITY_ID_FORMAT.format(dev_id)
|
self.entity_id = ENTITY_ID_FORMAT.format(dev_id)
|
||||||
|
|
||||||
@ -259,6 +277,8 @@ class Device(Entity):
|
|||||||
# detected anymore.
|
# detected anymore.
|
||||||
self.consider_home = consider_home
|
self.consider_home = consider_home
|
||||||
|
|
||||||
|
# Distance in meters
|
||||||
|
self.home_range = home_range
|
||||||
# Device ID
|
# Device ID
|
||||||
self.dev_id = dev_id
|
self.dev_id = dev_id
|
||||||
self.mac = mac
|
self.mac = mac
|
||||||
@ -273,6 +293,13 @@ class Device(Entity):
|
|||||||
self.config_picture = picture
|
self.config_picture = picture
|
||||||
self.away_hide = away_hide
|
self.away_hide = away_hide
|
||||||
|
|
||||||
|
@property
|
||||||
|
def gps_home(self):
|
||||||
|
""" Return if device is within range of home. """
|
||||||
|
distance = max(
|
||||||
|
0, self.hass.config.distance(*self.gps) - self.gps_accuracy)
|
||||||
|
return self.gps is not None and distance <= self.home_range
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
""" Returns the name of the entity. """
|
""" Returns the name of the entity. """
|
||||||
@ -292,8 +319,12 @@ class Device(Entity):
|
|||||||
attr[ATTR_ENTITY_PICTURE] = self.config_picture
|
attr[ATTR_ENTITY_PICTURE] = self.config_picture
|
||||||
|
|
||||||
if self.gps:
|
if self.gps:
|
||||||
attr[ATTR_LATITUDE] = self.gps[0],
|
attr[ATTR_LATITUDE] = self.gps[0]
|
||||||
attr[ATTR_LONGITUDE] = self.gps[1],
|
attr[ATTR_LONGITUDE] = self.gps[1]
|
||||||
|
attr[ATTR_GPS_ACCURACY] = self.gps_accuracy
|
||||||
|
|
||||||
|
if self.battery:
|
||||||
|
attr[ATTR_BATTERY] = self.battery
|
||||||
|
|
||||||
return attr
|
return attr
|
||||||
|
|
||||||
@ -302,12 +333,23 @@ class Device(Entity):
|
|||||||
""" If device should be hidden. """
|
""" If device should be hidden. """
|
||||||
return self.away_hide and self.state != STATE_HOME
|
return self.away_hide and self.state != STATE_HOME
|
||||||
|
|
||||||
def seen(self, host_name=None, location_name=None, gps=None):
|
def seen(self, host_name=None, location_name=None, gps=None,
|
||||||
|
gps_accuracy=0, battery=None):
|
||||||
""" Mark the device as seen. """
|
""" Mark the device as seen. """
|
||||||
self.last_seen = dt_util.utcnow()
|
self.last_seen = dt_util.utcnow()
|
||||||
self.host_name = host_name
|
self.host_name = host_name
|
||||||
self.location_name = location_name
|
self.location_name = location_name
|
||||||
self.gps = gps
|
self.gps_accuracy = gps_accuracy or 0
|
||||||
|
self.battery = battery
|
||||||
|
if gps is None:
|
||||||
|
self.gps = None
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
self.gps = tuple(float(val) for val in gps)
|
||||||
|
except ValueError:
|
||||||
|
_LOGGER.warning('Could not parse gps value for %s: %s',
|
||||||
|
self.dev_id, gps)
|
||||||
|
self.gps = None
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
def stale(self, now=None):
|
def stale(self, now=None):
|
||||||
@ -321,6 +363,16 @@ class Device(Entity):
|
|||||||
return
|
return
|
||||||
elif self.location_name:
|
elif self.location_name:
|
||||||
self._state = self.location_name
|
self._state = self.location_name
|
||||||
|
elif self.gps is not None:
|
||||||
|
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():
|
elif self.stale():
|
||||||
self._state = STATE_NOT_HOME
|
self._state = STATE_NOT_HOME
|
||||||
self.last_update_home = False
|
self.last_update_home = False
|
||||||
@ -338,18 +390,18 @@ def convert_csv_config(csv_path, yaml_path):
|
|||||||
(util.slugify(row['name']) or DEVICE_DEFAULT_NAME).lower(),
|
(util.slugify(row['name']) or DEVICE_DEFAULT_NAME).lower(),
|
||||||
used_ids)
|
used_ids)
|
||||||
used_ids.add(dev_id)
|
used_ids.add(dev_id)
|
||||||
device = Device(None, None, row['track'] == '1', dev_id,
|
device = Device(None, None, None, row['track'] == '1', dev_id,
|
||||||
row['device'], row['name'], row['picture'])
|
row['device'], row['name'], row['picture'])
|
||||||
update_config(yaml_path, dev_id, device)
|
update_config(yaml_path, dev_id, device)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def load_config(path, hass, consider_home):
|
def load_config(path, hass, consider_home, home_range):
|
||||||
""" Load devices from YAML config file. """
|
""" Load devices from YAML config file. """
|
||||||
if not os.path.isfile(path):
|
if not os.path.isfile(path):
|
||||||
return []
|
return []
|
||||||
return [
|
return [
|
||||||
Device(hass, consider_home, device.get('track', False),
|
Device(hass, consider_home, home_range, device.get('track', False),
|
||||||
str(dev_id).lower(), str(device.get('mac')).upper(),
|
str(dev_id).lower(), str(device.get('mac')).upper(),
|
||||||
device.get('name'), device.get('picture'),
|
device.get('name'), device.get('picture'),
|
||||||
device.get(CONF_AWAY_HIDE, DEFAULT_AWAY_HIDE))
|
device.get(CONF_AWAY_HIDE, DEFAULT_AWAY_HIDE))
|
||||||
|
50
homeassistant/components/device_tracker/demo.py
Normal file
50
homeassistant/components/device_tracker/demo.py
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
"""
|
||||||
|
homeassistant.components.device_tracker.demo
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Demo platform for the device tracker.
|
||||||
|
|
||||||
|
device_tracker:
|
||||||
|
platform: demo
|
||||||
|
"""
|
||||||
|
import random
|
||||||
|
|
||||||
|
from homeassistant.components.device_tracker import DOMAIN
|
||||||
|
|
||||||
|
|
||||||
|
def setup_scanner(hass, config, see):
|
||||||
|
""" Set up a demo tracker. """
|
||||||
|
|
||||||
|
def offset():
|
||||||
|
""" Return random offset. """
|
||||||
|
return (random.randrange(500, 2000)) / 2e5 * random.choice((-1, 1))
|
||||||
|
|
||||||
|
def random_see(dev_id, name):
|
||||||
|
""" Randomize a sighting. """
|
||||||
|
see(
|
||||||
|
dev_id=dev_id,
|
||||||
|
host_name=name,
|
||||||
|
gps=(hass.config.latitude + offset(),
|
||||||
|
hass.config.longitude + offset()),
|
||||||
|
gps_accuracy=random.randrange(50, 150),
|
||||||
|
battery=random.randrange(10, 90)
|
||||||
|
)
|
||||||
|
|
||||||
|
def observe(call=None):
|
||||||
|
""" Observe three entities. """
|
||||||
|
random_see('demo_paulus', 'Paulus')
|
||||||
|
random_see('demo_anne_therese', 'Anne Therese')
|
||||||
|
|
||||||
|
observe()
|
||||||
|
|
||||||
|
see(
|
||||||
|
dev_id='demo_home_boy',
|
||||||
|
host_name='Home Boy',
|
||||||
|
gps=[hass.config.latitude - 0.00002, hass.config.longitude + 0.00002],
|
||||||
|
gps_accuracy=20,
|
||||||
|
battery=53
|
||||||
|
)
|
||||||
|
|
||||||
|
hass.services.register(DOMAIN, 'demo', observe)
|
||||||
|
|
||||||
|
return True
|
54
homeassistant/components/device_tracker/owntracks.py
Normal file
54
homeassistant/components/device_tracker/owntracks.py
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
"""
|
||||||
|
homeassistant.components.device_tracker.owntracks
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
OwnTracks platform for the device tracker.
|
||||||
|
|
||||||
|
device_tracker:
|
||||||
|
platform: owntracks
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import homeassistant.components.mqtt as mqtt
|
||||||
|
|
||||||
|
DEPENDENCIES = ['mqtt']
|
||||||
|
|
||||||
|
LOCATION_TOPIC = 'owntracks/+/+'
|
||||||
|
|
||||||
|
|
||||||
|
def setup_scanner(hass, config, see):
|
||||||
|
""" Set up a OwnTracksks tracker. """
|
||||||
|
|
||||||
|
def owntracks_location_update(topic, payload, qos):
|
||||||
|
""" MQTT message received. """
|
||||||
|
|
||||||
|
# Docs on available data:
|
||||||
|
# http://owntracks.org/booklet/tech/json/#_typelocation
|
||||||
|
try:
|
||||||
|
data = json.loads(payload)
|
||||||
|
except ValueError:
|
||||||
|
# If invalid JSON
|
||||||
|
logging.getLogger(__name__).error(
|
||||||
|
'Unable to parse payload as JSON: %s', payload)
|
||||||
|
return
|
||||||
|
|
||||||
|
if not isinstance(data, dict) or data.get('_type') != 'location':
|
||||||
|
return
|
||||||
|
|
||||||
|
parts = topic.split('/')
|
||||||
|
kwargs = {
|
||||||
|
'dev_id': '{}_{}'.format(parts[1], parts[2]),
|
||||||
|
'host_name': parts[1],
|
||||||
|
'gps': (data['lat'], data['lon']),
|
||||||
|
}
|
||||||
|
if 'acc' in data:
|
||||||
|
kwargs['gps_accuracy'] = data['acc']
|
||||||
|
if 'batt' in data:
|
||||||
|
kwargs['battery'] = data['batt']
|
||||||
|
|
||||||
|
see(**kwargs)
|
||||||
|
|
||||||
|
mqtt.subscribe(hass, LOCATION_TOPIC, owntracks_location_update, 1)
|
||||||
|
|
||||||
|
return True
|
@ -19,7 +19,7 @@ from homeassistant.const import (
|
|||||||
|
|
||||||
DOMAIN = "discovery"
|
DOMAIN = "discovery"
|
||||||
DEPENDENCIES = []
|
DEPENDENCIES = []
|
||||||
REQUIREMENTS = ['netdisco==0.4']
|
REQUIREMENTS = ['netdisco==0.4.2']
|
||||||
|
|
||||||
SCAN_INTERVAL = 300 # seconds
|
SCAN_INTERVAL = 300 # seconds
|
||||||
|
|
||||||
|
@ -21,7 +21,8 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
FRONTEND_URLS = [
|
FRONTEND_URLS = [
|
||||||
URL_ROOT, '/logbook', '/history', '/devService', '/devState', '/devEvent']
|
URL_ROOT, '/logbook', '/history', '/map', '/devService', '/devState',
|
||||||
|
'/devEvent']
|
||||||
STATES_URL = re.compile(r'/states(/([a-zA-Z\._\-0-9/]+)|)')
|
STATES_URL = re.compile(r'/states(/([a-zA-Z\._\-0-9/]+)|)')
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,2 +1,2 @@
|
|||||||
""" DO NOT MODIFY. Auto-generated by build_frontend script """
|
""" DO NOT MODIFY. Auto-generated by build_frontend script """
|
||||||
VERSION = "5f35285bc502e3f69f564240fee04baa"
|
VERSION = "c4722afa376379bc4457d54bb9a38cee"
|
||||||
|
File diff suppressed because one or more lines are too long
@ -1 +1 @@
|
|||||||
Subproject commit 68f6c6ae5d37a1f0fcd1c36a8803581f9367ac5f
|
Subproject commit c8d99bc3ea21cdd7bfb39e7700f92ed09f4b9efd
|
Binary file not shown.
After Width: | Height: | Size: 2.8 KiB |
Binary file not shown.
After Width: | Height: | Size: 1.5 KiB |
Binary file not shown.
After Width: | Height: | Size: 3.9 KiB |
Binary file not shown.
After Width: | Height: | Size: 1.7 KiB |
Binary file not shown.
After Width: | Height: | Size: 797 B |
@ -12,7 +12,8 @@ from homeassistant.helpers.entity import Entity
|
|||||||
import homeassistant.util as util
|
import homeassistant.util as util
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_ENTITY_ID, STATE_ON, STATE_OFF,
|
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"
|
DOMAIN = "group"
|
||||||
DEPENDENCIES = []
|
DEPENDENCIES = []
|
||||||
@ -22,7 +23,8 @@ ENTITY_ID_FORMAT = DOMAIN + ".{}"
|
|||||||
ATTR_AUTO = "auto"
|
ATTR_AUTO = "auto"
|
||||||
|
|
||||||
# List of ON/OFF state tuples for groupable states
|
# 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):
|
def _get_group_on_off(state):
|
||||||
|
@ -232,7 +232,12 @@ class RequestHandler(SimpleHTTPRequestHandler):
|
|||||||
|
|
||||||
def log_message(self, fmt, *arguments):
|
def log_message(self, fmt, *arguments):
|
||||||
""" Redirect built-in log to HA logging """
|
""" 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
|
def _handle_request(self, method): # pylint: disable=too-many-branches
|
||||||
""" Does some common checks and calls appropriate method. """
|
""" Does some common checks and calls appropriate method. """
|
||||||
|
@ -52,14 +52,14 @@ import logging
|
|||||||
import os
|
import os
|
||||||
import csv
|
import csv
|
||||||
|
|
||||||
from homeassistant.helpers.entity_component import EntityComponent
|
from homeassistant.components import group, discovery, wink, isy994
|
||||||
from homeassistant.helpers.entity import ToggleEntity
|
from homeassistant.config import load_yaml_config_file
|
||||||
|
|
||||||
import homeassistant.util as util
|
|
||||||
import homeassistant.util.color as color_util
|
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID)
|
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"
|
DOMAIN = "light"
|
||||||
@ -275,11 +275,13 @@ def setup(hass, config):
|
|||||||
light.update_ha_state(True)
|
light.update_ha_state(True)
|
||||||
|
|
||||||
# Listen for light on and light off service calls
|
# Listen for light on and light off service calls
|
||||||
hass.services.register(DOMAIN, SERVICE_TURN_ON,
|
descriptions = load_yaml_config_file(
|
||||||
handle_light_service)
|
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,
|
hass.services.register(DOMAIN, SERVICE_TURN_OFF, handle_light_service,
|
||||||
handle_light_service)
|
descriptions.get(SERVICE_TURN_OFF))
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@ -19,11 +19,15 @@ configuration.yaml file.
|
|||||||
|
|
||||||
light:
|
light:
|
||||||
platform: limitlessled
|
platform: limitlessled
|
||||||
host: 192.168.1.10
|
bridges:
|
||||||
group_1_name: Living Room
|
- host: 192.168.1.10
|
||||||
group_2_name: Bedroom
|
group_1_name: Living Room
|
||||||
group_3_name: Office
|
group_2_name: Bedroom
|
||||||
group_4_name: Kitchen
|
group_3_name: Office
|
||||||
|
group_3_type: white
|
||||||
|
group_4_name: Kitchen
|
||||||
|
- host: 192.168.1.11
|
||||||
|
group_2_name: Basement
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
@ -33,19 +37,30 @@ from homeassistant.components.light import (Light, ATTR_BRIGHTNESS,
|
|||||||
from homeassistant.util.color import color_RGB_to_xy
|
from homeassistant.util.color import color_RGB_to_xy
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_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):
|
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||||
""" Gets the LimitlessLED lights. """
|
""" Gets the LimitlessLED lights. """
|
||||||
import ledcontroller
|
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 = []
|
lights = []
|
||||||
for i in range(1, 5):
|
for bridge in bridges:
|
||||||
if 'group_%d_name' % (i) in config:
|
for i in range(1, 5):
|
||||||
lights.append(LimitlessLED(led, i, config['group_%d_name' % (i)]))
|
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)
|
add_devices_callback(lights)
|
||||||
|
|
||||||
@ -53,15 +68,57 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
|||||||
class LimitlessLED(Light):
|
class LimitlessLED(Light):
|
||||||
""" Represents a LimitlessLED light """
|
""" Represents a LimitlessLED light """
|
||||||
|
|
||||||
def __init__(self, led, group, name):
|
@staticmethod
|
||||||
self.led = led
|
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.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.
|
# 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._name = name or DEVICE_DEFAULT_NAME
|
||||||
self._state = False
|
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._brightness = 100
|
||||||
self._xy_color = color_RGB_to_xy(255, 255, 255)
|
self._xy_color = color_RGB_to_xy(255, 255, 255)
|
||||||
|
|
||||||
@ -87,16 +144,6 @@ class LimitlessLED(Light):
|
|||||||
((0xE6, 0xE6, 0xFA), 'lavendar'),
|
((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
|
@property
|
||||||
def brightness(self):
|
def brightness(self):
|
||||||
return self._brightness
|
return self._brightness
|
||||||
@ -117,11 +164,6 @@ class LimitlessLED(Light):
|
|||||||
# First candidate in the sorted list is closest to desired color:
|
# First candidate in the sorted list is closest to desired color:
|
||||||
return sorted(candidates)[0][1]
|
return sorted(candidates)[0][1]
|
||||||
|
|
||||||
@property
|
|
||||||
def is_on(self):
|
|
||||||
""" True if device is on. """
|
|
||||||
return self._state
|
|
||||||
|
|
||||||
def turn_on(self, **kwargs):
|
def turn_on(self, **kwargs):
|
||||||
""" Turn the device on. """
|
""" Turn the device on. """
|
||||||
self._state = True
|
self._state = True
|
||||||
@ -132,12 +174,21 @@ class LimitlessLED(Light):
|
|||||||
if ATTR_XY_COLOR in kwargs:
|
if ATTR_XY_COLOR in kwargs:
|
||||||
self._xy_color = kwargs[ATTR_XY_COLOR]
|
self._xy_color = kwargs[ATTR_XY_COLOR]
|
||||||
|
|
||||||
self.led.set_color(self._xy_to_led_color(self._xy_color), self.group)
|
self.pool.execute(self.controller_id, "set_color",
|
||||||
self.led.set_brightness(self._brightness / 255.0, self.group)
|
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()
|
self.update_ha_state()
|
||||||
|
|
||||||
def turn_off(self, **kwargs):
|
|
||||||
""" Turn the device off. """
|
class WhiteLimitlessLED(LimitlessLED):
|
||||||
self._state = False
|
""" Represents a White LimitlessLED light """
|
||||||
self.led.off(self.group)
|
|
||||||
|
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()
|
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,12 +6,14 @@ Support for Tellstick lights.
|
|||||||
import logging
|
import logging
|
||||||
# pylint: disable=no-name-in-module, import-error
|
# pylint: disable=no-name-in-module, import-error
|
||||||
from homeassistant.components.light import Light, ATTR_BRIGHTNESS
|
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
|
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
|
||||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||||
""" Find and return Tellstick lights. """
|
""" Find and return Tellstick lights. """
|
||||||
|
|
||||||
@ -22,13 +24,32 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
|||||||
"Failed to import tellcore")
|
"Failed to import tellcore")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
core = telldus.TelldusCore()
|
core = telldus.TelldusCore(callback_dispatcher=DirectCallbackDispatcher())
|
||||||
|
|
||||||
switches_and_lights = core.devices()
|
switches_and_lights = core.devices()
|
||||||
lights = []
|
lights = []
|
||||||
|
|
||||||
for switch in switches_and_lights:
|
for switch in switches_and_lights:
|
||||||
if switch.methods(tellcore_constants.TELLSTICK_DIM):
|
if switch.methods(tellcore_constants.TELLSTICK_DIM):
|
||||||
lights.append(TellstickLight(switch))
|
lights.append(TellstickLight(switch))
|
||||||
|
|
||||||
|
def _device_event_callback(id_, method, data, cid):
|
||||||
|
""" 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
|
||||||
|
|
||||||
|
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)
|
add_devices_callback(lights)
|
||||||
|
|
||||||
|
|
||||||
@ -40,15 +61,15 @@ class TellstickLight(Light):
|
|||||||
tellcore_constants.TELLSTICK_UP |
|
tellcore_constants.TELLSTICK_UP |
|
||||||
tellcore_constants.TELLSTICK_DOWN)
|
tellcore_constants.TELLSTICK_DOWN)
|
||||||
|
|
||||||
def __init__(self, tellstick):
|
def __init__(self, tellstick_device):
|
||||||
self.tellstick = tellstick
|
self.tellstick_device = tellstick_device
|
||||||
self.state_attr = {ATTR_FRIENDLY_NAME: tellstick.name}
|
self.state_attr = {ATTR_FRIENDLY_NAME: tellstick_device.name}
|
||||||
self._brightness = 0
|
self._brightness = 0
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
""" Returns the name of the switch if any. """
|
""" Returns the name of the switch if any. """
|
||||||
return self.tellstick.name
|
return self.tellstick_device.name
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_on(self):
|
def is_on(self):
|
||||||
@ -62,8 +83,9 @@ class TellstickLight(Light):
|
|||||||
|
|
||||||
def turn_off(self, **kwargs):
|
def turn_off(self, **kwargs):
|
||||||
""" Turns the switch off. """
|
""" Turns the switch off. """
|
||||||
self.tellstick.turn_off()
|
self.tellstick_device.turn_off()
|
||||||
self._brightness = 0
|
self._brightness = 0
|
||||||
|
self.update_ha_state()
|
||||||
|
|
||||||
def turn_on(self, **kwargs):
|
def turn_on(self, **kwargs):
|
||||||
""" Turns the switch on. """
|
""" Turns the switch on. """
|
||||||
@ -74,11 +96,12 @@ class TellstickLight(Light):
|
|||||||
else:
|
else:
|
||||||
self._brightness = brightness
|
self._brightness = brightness
|
||||||
|
|
||||||
self.tellstick.dim(self._brightness)
|
self.tellstick_device.dim(self._brightness)
|
||||||
|
self.update_ha_state()
|
||||||
|
|
||||||
def update(self):
|
def update(self):
|
||||||
""" Update state of the light. """
|
""" Update state of the light. """
|
||||||
last_command = self.tellstick.last_sent_command(
|
last_command = self.tellstick_device.last_sent_command(
|
||||||
self.last_sent_command_mask)
|
self.last_sent_command_mask)
|
||||||
|
|
||||||
if last_command == tellcore_constants.TELLSTICK_TURNON:
|
if last_command == tellcore_constants.TELLSTICK_TURNON:
|
||||||
@ -88,6 +111,11 @@ class TellstickLight(Light):
|
|||||||
elif (last_command == tellcore_constants.TELLSTICK_DIM or
|
elif (last_command == tellcore_constants.TELLSTICK_DIM or
|
||||||
last_command == tellcore_constants.TELLSTICK_UP or
|
last_command == tellcore_constants.TELLSTICK_UP or
|
||||||
last_command == tellcore_constants.TELLSTICK_DOWN):
|
last_command == tellcore_constants.TELLSTICK_DOWN):
|
||||||
last_sent_value = self.tellstick.last_sent_value()
|
last_sent_value = self.tellstick_device.last_sent_value()
|
||||||
if last_sent_value is not None:
|
if last_sent_value is not None:
|
||||||
self._brightness = last_sent_value
|
self._brightness = last_sent_value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def should_poll(self):
|
||||||
|
""" Tells Home Assistant not to poll this entity. """
|
||||||
|
return False
|
||||||
|
@ -10,7 +10,7 @@ import re
|
|||||||
|
|
||||||
from homeassistant.core import State, DOMAIN as HA_DOMAIN
|
from homeassistant.core import State, DOMAIN as HA_DOMAIN
|
||||||
from homeassistant.const import (
|
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)
|
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, HTTP_BAD_REQUEST)
|
||||||
from homeassistant import util
|
from homeassistant import util
|
||||||
import homeassistant.util.dt as dt_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'))
|
to_state = State.from_dict(event.data.get('new_state'))
|
||||||
|
|
||||||
# if last_changed == last_updated only attributes have changed
|
# if last_changed != last_updated only attributes have changed
|
||||||
# we do not report on that yet.
|
# we do not report on that yet. Also filter auto groups.
|
||||||
if not to_state or \
|
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
|
continue
|
||||||
|
|
||||||
domain = to_state.domain
|
domain = to_state.domain
|
||||||
@ -218,10 +220,13 @@ def humanify(events):
|
|||||||
def _entry_message_from_state(domain, state):
|
def _entry_message_from_state(domain, state):
|
||||||
""" Convert a state to a message for the logbook. """
|
""" Convert a state to a message for the logbook. """
|
||||||
# We pass domain in so we don't have to split entity_id again
|
# We pass domain in so we don't have to split entity_id again
|
||||||
|
# pylint: disable=too-many-return-statements
|
||||||
|
|
||||||
if domain == 'device_tracker':
|
if domain == 'device_tracker':
|
||||||
return '{} home'.format(
|
if state.state == STATE_NOT_HOME:
|
||||||
'arrived' if state.state == STATE_HOME else 'left')
|
return 'is away'
|
||||||
|
else:
|
||||||
|
return 'is at {}'.format(state.state)
|
||||||
|
|
||||||
elif domain == 'sun':
|
elif domain == 'sun':
|
||||||
if state.state == sun.STATE_ABOVE_HORIZON:
|
if state.state == sun.STATE_ABOVE_HORIZON:
|
||||||
|
@ -5,8 +5,10 @@ homeassistant.components.media_player
|
|||||||
Component to interface with various media players.
|
Component to interface with various media players.
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
|
|
||||||
from homeassistant.components import discovery
|
from homeassistant.components import discovery
|
||||||
|
from homeassistant.config import load_yaml_config_file
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
from homeassistant.helpers.entity_component import EntityComponent
|
from homeassistant.helpers.entity_component import EntityComponent
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
@ -186,6 +188,9 @@ def setup(hass, config):
|
|||||||
|
|
||||||
component.setup(config)
|
component.setup(config)
|
||||||
|
|
||||||
|
descriptions = load_yaml_config_file(
|
||||||
|
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
||||||
|
|
||||||
def media_player_service_handler(service):
|
def media_player_service_handler(service):
|
||||||
""" Maps services to methods on MediaPlayerDevice. """
|
""" Maps services to methods on MediaPlayerDevice. """
|
||||||
target_players = component.extract_from_service(service)
|
target_players = component.extract_from_service(service)
|
||||||
@ -199,7 +204,8 @@ def setup(hass, config):
|
|||||||
player.update_ha_state(True)
|
player.update_ha_state(True)
|
||||||
|
|
||||||
for service in SERVICE_TO_METHOD:
|
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):
|
def volume_set_service(service):
|
||||||
""" Set specified volume on the media player. """
|
""" Set specified volume on the media player. """
|
||||||
@ -216,7 +222,8 @@ def setup(hass, config):
|
|||||||
if player.should_poll:
|
if player.should_poll:
|
||||||
player.update_ha_state(True)
|
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):
|
def volume_mute_service(service):
|
||||||
""" Mute (true) or unmute (false) the media player. """
|
""" Mute (true) or unmute (false) the media player. """
|
||||||
@ -233,7 +240,8 @@ def setup(hass, config):
|
|||||||
if player.should_poll:
|
if player.should_poll:
|
||||||
player.update_ha_state(True)
|
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):
|
def media_seek_service(service):
|
||||||
""" Seek to a position. """
|
""" Seek to a position. """
|
||||||
@ -250,7 +258,8 @@ def setup(hass, config):
|
|||||||
if player.should_poll:
|
if player.should_poll:
|
||||||
player.update_ha_state(True)
|
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):
|
def play_youtube_video_service(service, media_id=None):
|
||||||
""" Plays specified media_id on the media player. """
|
""" Plays specified media_id on the media player. """
|
||||||
@ -268,14 +277,17 @@ def setup(hass, config):
|
|||||||
|
|
||||||
hass.services.register(
|
hass.services.register(
|
||||||
DOMAIN, "start_fireplace",
|
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(
|
hass.services.register(
|
||||||
DOMAIN, "start_epic_sax",
|
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(
|
hass.services.register(
|
||||||
DOMAIN, SERVICE_YOUTUBE_VIDEO, play_youtube_video_service)
|
DOMAIN, SERVICE_YOUTUBE_VIDEO, play_youtube_video_service,
|
||||||
|
descriptions.get(SERVICE_YOUTUBE_VIDEO))
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
237
homeassistant/components/media_player/plex.py
Normal file
237
homeassistant/components/media_player/plex.py
Normal file
@ -0,0 +1,237 @@
|
|||||||
|
"""
|
||||||
|
homeassistant.components.media_player.plex
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
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
|
||||||
|
"""
|
||||||
|
|
||||||
|
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_OFF, STATE_UNKNOWN)
|
||||||
|
import homeassistant.util as util
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
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()
|
||||||
|
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 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.device.name or self.device.product or self.device.device
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state(self):
|
||||||
|
""" Returns the state of the device. """
|
||||||
|
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):
|
||||||
|
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.session is not None:
|
||||||
|
return self.session.ratingKey
|
||||||
|
|
||||||
|
@property
|
||||||
|
def media_content_type(self):
|
||||||
|
""" Content type of current playing media. """
|
||||||
|
if self.session is None:
|
||||||
|
return None
|
||||||
|
media_type = self.session.type
|
||||||
|
if media_type == 'episode':
|
||||||
|
return MEDIA_TYPE_TVSHOW
|
||||||
|
elif media_type == 'movie':
|
||||||
|
return MEDIA_TYPE_VIDEO
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def media_duration(self):
|
||||||
|
""" Duration of current playing media in seconds. """
|
||||||
|
if self.session is not None:
|
||||||
|
return self.session.duration
|
||||||
|
|
||||||
|
@property
|
||||||
|
def media_image_url(self):
|
||||||
|
""" Image url of current playing media. """
|
||||||
|
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.session is not None:
|
||||||
|
return self.session.title
|
||||||
|
|
||||||
|
@property
|
||||||
|
def media_season(self):
|
||||||
|
""" 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)"""
|
||||||
|
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) """
|
||||||
|
from plexapi.video import Show
|
||||||
|
if isinstance(self.session, Show):
|
||||||
|
return self.session.index
|
||||||
|
|
||||||
|
@property
|
||||||
|
def supported_media_commands(self):
|
||||||
|
""" Flags of media commands that are supported. """
|
||||||
|
return SUPPORT_PLEX
|
||||||
|
|
||||||
|
def media_play(self):
|
||||||
|
""" media_play media player. """
|
||||||
|
self.device.play({'type': 'video'})
|
||||||
|
|
||||||
|
def media_pause(self):
|
||||||
|
""" media_pause media player. """
|
||||||
|
self.device.pause({'type': 'video'})
|
||||||
|
|
||||||
|
def media_next_track(self):
|
||||||
|
""" Send next track command. """
|
||||||
|
self.device.skipNext({'type': 'video'})
|
||||||
|
|
||||||
|
def media_previous_track(self):
|
||||||
|
""" Send previous track command. """
|
||||||
|
self.device.skipPrevious({'type': 'video'})
|
0
homeassistant/components/media_player/services.yaml
Normal file
0
homeassistant/components/media_player/services.yaml
Normal file
@ -23,6 +23,7 @@ mqtt:
|
|||||||
keepalive: 60
|
keepalive: 60
|
||||||
username: your_username
|
username: your_username
|
||||||
password: your_secret_password
|
password: your_secret_password
|
||||||
|
certificate: /home/paulus/dev/addtrustexternalcaroot.crt
|
||||||
|
|
||||||
Variables:
|
Variables:
|
||||||
|
|
||||||
@ -42,8 +43,13 @@ Default is a random generated one.
|
|||||||
keepalive
|
keepalive
|
||||||
*Optional
|
*Optional
|
||||||
The keep alive in seconds for this client. Default is 60.
|
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 logging
|
||||||
|
import os
|
||||||
import socket
|
import socket
|
||||||
|
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
@ -74,6 +80,7 @@ CONF_CLIENT_ID = 'client_id'
|
|||||||
CONF_KEEPALIVE = 'keepalive'
|
CONF_KEEPALIVE = 'keepalive'
|
||||||
CONF_USERNAME = 'username'
|
CONF_USERNAME = 'username'
|
||||||
CONF_PASSWORD = 'password'
|
CONF_PASSWORD = 'password'
|
||||||
|
CONF_CERTIFICATE = 'certificate'
|
||||||
|
|
||||||
ATTR_TOPIC = 'topic'
|
ATTR_TOPIC = 'topic'
|
||||||
ATTR_PAYLOAD = 'payload'
|
ATTR_PAYLOAD = 'payload'
|
||||||
@ -119,11 +126,18 @@ def setup(hass, config):
|
|||||||
keepalive = util.convert(conf.get(CONF_KEEPALIVE), int, DEFAULT_KEEPALIVE)
|
keepalive = util.convert(conf.get(CONF_KEEPALIVE), int, DEFAULT_KEEPALIVE)
|
||||||
username = util.convert(conf.get(CONF_USERNAME), str)
|
username = util.convert(conf.get(CONF_USERNAME), str)
|
||||||
password = util.convert(conf.get(CONF_PASSWORD), 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
|
global MQTT_CLIENT
|
||||||
try:
|
try:
|
||||||
MQTT_CLIENT = MQTT(hass, broker, port, client_id, keepalive, username,
|
MQTT_CLIENT = MQTT(hass, broker, port, client_id, keepalive, username,
|
||||||
password)
|
password, certificate)
|
||||||
except socket.error:
|
except socket.error:
|
||||||
_LOGGER.exception("Can't connect to the broker. "
|
_LOGGER.exception("Can't connect to the broker. "
|
||||||
"Please check your settings and the broker "
|
"Please check your settings and the broker "
|
||||||
@ -161,7 +175,7 @@ def setup(hass, config):
|
|||||||
class MQTT(object): # pragma: no cover
|
class MQTT(object): # pragma: no cover
|
||||||
""" Implements messaging service for MQTT. """
|
""" Implements messaging service for MQTT. """
|
||||||
def __init__(self, hass, broker, port, client_id, keepalive, username,
|
def __init__(self, hass, broker, port, client_id, keepalive, username,
|
||||||
password):
|
password, certificate):
|
||||||
import paho.mqtt.client as mqtt
|
import paho.mqtt.client as mqtt
|
||||||
|
|
||||||
self.hass = hass
|
self.hass = hass
|
||||||
@ -172,8 +186,12 @@ class MQTT(object): # pragma: no cover
|
|||||||
self._mqttc = mqtt.Client()
|
self._mqttc = mqtt.Client()
|
||||||
else:
|
else:
|
||||||
self._mqttc = mqtt.Client(client_id)
|
self._mqttc = mqtt.Client(client_id)
|
||||||
|
|
||||||
if username is not None:
|
if username is not None:
|
||||||
self._mqttc.username_pw_set(username, password)
|
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_subscribe = self._mqtt_on_subscribe
|
||||||
self._mqttc.on_unsubscribe = self._mqtt_on_unsubscribe
|
self._mqttc.on_unsubscribe = self._mqtt_on_unsubscribe
|
||||||
self._mqttc.on_connect = self._mqtt_on_connect
|
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):
|
def _mqtt_on_connect(self, mqttc, obj, flags, result_code):
|
||||||
""" On connect, resubscribe to all topics we were subscribed to. """
|
""" 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
|
old_topics = self.topics
|
||||||
self._progress = {}
|
self._progress = {}
|
||||||
self.topics = {}
|
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
|
from functools import partial
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
|
|
||||||
|
from homeassistant.config import load_yaml_config_file
|
||||||
from homeassistant.loader import get_component
|
from homeassistant.loader import get_component
|
||||||
from homeassistant.helpers import config_per_platform
|
from homeassistant.helpers import config_per_platform
|
||||||
|
|
||||||
@ -36,6 +38,9 @@ def setup(hass, config):
|
|||||||
""" Sets up notify services. """
|
""" Sets up notify services. """
|
||||||
success = False
|
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):
|
for platform, p_config in config_per_platform(config, DOMAIN, _LOGGER):
|
||||||
# get platform
|
# get platform
|
||||||
notify_implementation = get_component(
|
notify_implementation = get_component(
|
||||||
@ -69,7 +74,8 @@ def setup(hass, config):
|
|||||||
# register service
|
# register service
|
||||||
service_call_handler = partial(notify_message, notify_service)
|
service_call_handler = partial(notify_message, notify_service)
|
||||||
service_notify = p_config.get(CONF_NAME, SERVICE_NOTIFY)
|
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
|
success = True
|
||||||
|
|
||||||
return success
|
return success
|
||||||
|
0
homeassistant/components/notify/services.yaml
Normal file
0
homeassistant/components/notify/services.yaml
Normal file
@ -140,13 +140,19 @@ class MailNotificationService(BaseNotificationService):
|
|||||||
self.username = username
|
self.username = username
|
||||||
self.password = password
|
self.password = password
|
||||||
self.recipient = recipient
|
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 = smtplib.SMTP(self._server, self._port)
|
||||||
self.mail.ehlo_or_helo_if_needed()
|
self.mail.ehlo_or_helo_if_needed()
|
||||||
if self.starttls == 1:
|
if self.starttls == 1:
|
||||||
self.mail.starttls()
|
self.mail.starttls()
|
||||||
self.mail.ehlo()
|
self.mail.ehlo()
|
||||||
|
|
||||||
self.mail.login(self.username, self.password)
|
self.mail.login(self.username, self.password)
|
||||||
|
|
||||||
def send_message(self, message="", **kwargs):
|
def send_message(self, message="", **kwargs):
|
||||||
@ -160,4 +166,12 @@ class MailNotificationService(BaseNotificationService):
|
|||||||
msg['From'] = self._sender
|
msg['From'] = self._sender
|
||||||
msg['X-Mailer'] = 'HomeAssistant'
|
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()
|
||||||
|
@ -33,7 +33,7 @@ ATTR_ACTIVE_REQUESTED = "active_requested"
|
|||||||
|
|
||||||
CONF_ENTITIES = "entities"
|
CONF_ENTITIES = "entities"
|
||||||
|
|
||||||
SceneConfig = namedtuple('SceneConfig', ['name', 'states'])
|
SceneConfig = namedtuple('SceneConfig', ['name', 'states', 'fuzzy_match'])
|
||||||
|
|
||||||
|
|
||||||
def setup(hass, config):
|
def setup(hass, config):
|
||||||
@ -71,6 +71,15 @@ def setup(hass, config):
|
|||||||
def _process_config(scene_config):
|
def _process_config(scene_config):
|
||||||
""" Process passed in config into a format to work with. """
|
""" Process passed in config into a format to work with. """
|
||||||
name = scene_config.get('name')
|
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 = {}
|
states = {}
|
||||||
c_entities = dict(scene_config.get(CONF_ENTITIES, {}))
|
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)
|
states[entity_id.lower()] = State(entity_id, state, attributes)
|
||||||
|
|
||||||
return SceneConfig(name, states)
|
return SceneConfig(name, states, fuzzy_match)
|
||||||
|
|
||||||
|
|
||||||
class Scene(ToggleEntity):
|
class Scene(ToggleEntity):
|
||||||
@ -179,9 +188,31 @@ class Scene(ToggleEntity):
|
|||||||
state = self.scene_config.states.get(cur_state and cur_state.entity_id)
|
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
|
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()))
|
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):
|
def _reproduce_state(self, states):
|
||||||
""" Wraps reproduce state with Scence specific logic. """
|
""" Wraps reproduce state with Scence specific logic. """
|
||||||
self.ignore_updates = True
|
self.ignore_updates = True
|
||||||
|
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,7 +3,8 @@
|
|||||||
homeassistant.components.sensor.rpi_gpio
|
homeassistant.components.sensor.rpi_gpio
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
Allows to configure a binary state sensor using 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:
|
sensor:
|
||||||
platform: rpi_gpio
|
platform: rpi_gpio
|
||||||
|
@ -34,7 +34,7 @@ import homeassistant.util as util
|
|||||||
|
|
||||||
DatatypeDescription = namedtuple("DatatypeDescription", ['name', 'unit'])
|
DatatypeDescription = namedtuple("DatatypeDescription", ['name', 'unit'])
|
||||||
|
|
||||||
REQUIREMENTS = ['tellcore-py==1.0.4']
|
REQUIREMENTS = ['tellcore-py==1.1.2']
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=unused-argument
|
# 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))
|
@ -3,9 +3,11 @@ homeassistant.components.switch
|
|||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
Component to interface with various switches that can be controlled remotely.
|
Component to interface with various switches that can be controlled remotely.
|
||||||
"""
|
"""
|
||||||
import logging
|
|
||||||
from datetime import timedelta
|
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_component import EntityComponent
|
||||||
from homeassistant.helpers.entity import ToggleEntity
|
from homeassistant.helpers.entity import ToggleEntity
|
||||||
|
|
||||||
@ -83,8 +85,12 @@ def setup(hass, config):
|
|||||||
if switch.should_poll:
|
if switch.should_poll:
|
||||||
switch.update_ha_state(True)
|
switch.update_ha_state(True)
|
||||||
|
|
||||||
hass.services.register(DOMAIN, SERVICE_TURN_OFF, handle_switch_service)
|
descriptions = load_yaml_config_file(
|
||||||
hass.services.register(DOMAIN, SERVICE_TURN_ON, handle_switch_service)
|
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
|
return True
|
||||||
|
|
||||||
|
0
homeassistant/components/switch/services.yaml
Normal file
0
homeassistant/components/switch/services.yaml
Normal file
@ -11,14 +11,14 @@ signal_repetitions: 3
|
|||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from homeassistant.const import (EVENT_HOMEASSISTANT_STOP,
|
||||||
from homeassistant.const import ATTR_FRIENDLY_NAME
|
ATTR_FRIENDLY_NAME)
|
||||||
from homeassistant.helpers.entity import ToggleEntity
|
from homeassistant.helpers.entity import ToggleEntity
|
||||||
import tellcore.constants as tellcore_constants
|
import tellcore.constants as tellcore_constants
|
||||||
|
from tellcore.library import DirectCallbackDispatcher
|
||||||
SINGAL_REPETITIONS = 1
|
SINGAL_REPETITIONS = 1
|
||||||
|
|
||||||
REQUIREMENTS = ['tellcore-py==1.0.4']
|
REQUIREMENTS = ['tellcore-py==1.1.2']
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
@ -31,16 +31,34 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
|||||||
"Failed to import tellcore")
|
"Failed to import tellcore")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
core = telldus.TelldusCore(callback_dispatcher=DirectCallbackDispatcher())
|
||||||
|
|
||||||
signal_repetitions = config.get('signal_repetitions', SINGAL_REPETITIONS)
|
signal_repetitions = config.get('signal_repetitions', SINGAL_REPETITIONS)
|
||||||
|
|
||||||
core = telldus.TelldusCore()
|
|
||||||
switches_and_lights = core.devices()
|
switches_and_lights = core.devices()
|
||||||
|
|
||||||
switches = []
|
switches = []
|
||||||
|
|
||||||
for switch in switches_and_lights:
|
for switch in switches_and_lights:
|
||||||
if not switch.methods(tellcore_constants.TELLSTICK_DIM):
|
if not switch.methods(tellcore_constants.TELLSTICK_DIM):
|
||||||
switches.append(TellstickSwitchDevice(switch, signal_repetitions))
|
switches.append(
|
||||||
|
TellstickSwitchDevice(switch, signal_repetitions))
|
||||||
|
|
||||||
|
def _device_event_callback(id_, method, data, cid):
|
||||||
|
""" 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()
|
||||||
|
break
|
||||||
|
|
||||||
|
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)
|
add_devices_callback(switches)
|
||||||
|
|
||||||
@ -50,15 +68,20 @@ class TellstickSwitchDevice(ToggleEntity):
|
|||||||
last_sent_command_mask = (tellcore_constants.TELLSTICK_TURNON |
|
last_sent_command_mask = (tellcore_constants.TELLSTICK_TURNON |
|
||||||
tellcore_constants.TELLSTICK_TURNOFF)
|
tellcore_constants.TELLSTICK_TURNOFF)
|
||||||
|
|
||||||
def __init__(self, tellstick, signal_repetitions):
|
def __init__(self, tellstick_device, signal_repetitions):
|
||||||
self.tellstick = tellstick
|
self.tellstick_device = tellstick_device
|
||||||
self.state_attr = {ATTR_FRIENDLY_NAME: tellstick.name}
|
self.state_attr = {ATTR_FRIENDLY_NAME: tellstick_device.name}
|
||||||
self.signal_repetitions = signal_repetitions
|
self.signal_repetitions = signal_repetitions
|
||||||
|
|
||||||
|
@property
|
||||||
|
def should_poll(self):
|
||||||
|
""" Tells Home Assistant not to poll this entity. """
|
||||||
|
return False
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
""" Returns the name of the switch if any. """
|
""" Returns the name of the switch if any. """
|
||||||
return self.tellstick.name
|
return self.tellstick_device.name
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state_attributes(self):
|
def state_attributes(self):
|
||||||
@ -68,7 +91,7 @@ class TellstickSwitchDevice(ToggleEntity):
|
|||||||
@property
|
@property
|
||||||
def is_on(self):
|
def is_on(self):
|
||||||
""" True if switch is on. """
|
""" True if switch is on. """
|
||||||
last_command = self.tellstick.last_sent_command(
|
last_command = self.tellstick_device.last_sent_command(
|
||||||
self.last_sent_command_mask)
|
self.last_sent_command_mask)
|
||||||
|
|
||||||
return last_command == tellcore_constants.TELLSTICK_TURNON
|
return last_command == tellcore_constants.TELLSTICK_TURNON
|
||||||
@ -76,9 +99,11 @@ class TellstickSwitchDevice(ToggleEntity):
|
|||||||
def turn_on(self, **kwargs):
|
def turn_on(self, **kwargs):
|
||||||
""" Turns the switch on. """
|
""" Turns the switch on. """
|
||||||
for _ in range(self.signal_repetitions):
|
for _ in range(self.signal_repetitions):
|
||||||
self.tellstick.turn_on()
|
self.tellstick_device.turn_on()
|
||||||
|
self.update_ha_state()
|
||||||
|
|
||||||
def turn_off(self, **kwargs):
|
def turn_off(self, **kwargs):
|
||||||
""" Turns the switch off. """
|
""" Turns the switch off. """
|
||||||
for _ in range(self.signal_repetitions):
|
for _ in range(self.signal_repetitions):
|
||||||
self.tellstick.turn_off()
|
self.tellstick_device.turn_off()
|
||||||
|
self.update_ha_state()
|
||||||
|
@ -122,7 +122,7 @@ class VeraSwitch(ToggleEntity):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def state_attributes(self):
|
def state_attributes(self):
|
||||||
attr = super().state_attributes
|
attr = super().state_attributes or {}
|
||||||
|
|
||||||
if self.vera_device.has_battery:
|
if self.vera_device.has_battery:
|
||||||
attr[ATTR_BATTERY_LEVEL] = self.vera_device.battery_level + '%'
|
attr[ATTR_BATTERY_LEVEL] = self.vera_device.battery_level + '%'
|
||||||
|
@ -9,7 +9,7 @@ import logging
|
|||||||
from homeassistant.components.switch import SwitchDevice
|
from homeassistant.components.switch import SwitchDevice
|
||||||
from homeassistant.const import STATE_ON, STATE_OFF, STATE_STANDBY
|
from homeassistant.const import STATE_ON, STATE_OFF, STATE_STANDBY
|
||||||
|
|
||||||
REQUIREMENTS = ['pywemo==0.3']
|
REQUIREMENTS = ['pywemo==0.3.1']
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
@ -123,9 +123,14 @@ class WemoSwitch(SwitchDevice):
|
|||||||
|
|
||||||
def update(self):
|
def update(self):
|
||||||
""" Update WeMo state. """
|
""" Update WeMo state. """
|
||||||
self.wemo.get_state(True)
|
try:
|
||||||
if self.wemo.model_name == 'Insight':
|
self.wemo.get_state(True)
|
||||||
self.insight_params = self.wemo.insight_params
|
if self.wemo.model_name == 'Insight':
|
||||||
self.insight_params['standby_state'] = self.wemo.get_standby_state
|
self.insight_params = self.wemo.insight_params
|
||||||
elif self.wemo.model_name == 'Maker':
|
self.insight_params['standby_state'] = (
|
||||||
self.maker_params = self.wemo.maker_params
|
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.
|
Provides functionality to interact with thermostats.
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
|
|
||||||
from homeassistant.helpers.entity_component import EntityComponent
|
from homeassistant.helpers.entity_component import EntityComponent
|
||||||
|
|
||||||
|
from homeassistant.config import load_yaml_config_file
|
||||||
import homeassistant.util as util
|
import homeassistant.util as util
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
from homeassistant.helpers.temperature import convert
|
from homeassistant.helpers.temperature import convert
|
||||||
@ -101,11 +103,16 @@ def setup(hass, config):
|
|||||||
for thermostat in target_thermostats:
|
for thermostat in target_thermostats:
|
||||||
thermostat.update_ha_state(True)
|
thermostat.update_ha_state(True)
|
||||||
|
|
||||||
hass.services.register(
|
descriptions = load_yaml_config_file(
|
||||||
DOMAIN, SERVICE_SET_AWAY_MODE, thermostat_service)
|
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
||||||
|
|
||||||
hass.services.register(
|
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
|
return True
|
||||||
|
|
||||||
|
@ -190,6 +190,13 @@ class HeatControl(ThermostatDevice):
|
|||||||
if self._heater_manual_changed:
|
if self._heater_manual_changed:
|
||||||
self.set_temperature(None)
|
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):
|
def turn_away_mode_on(self):
|
||||||
""" Turns away mode on. """
|
""" Turns away mode on. """
|
||||||
self._away = True
|
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. """
|
""" Constants used by Home Assistant components. """
|
||||||
|
|
||||||
__version__ = "0.7.3"
|
__version__ = "0.7.4dev0"
|
||||||
|
|
||||||
# Can be used to specify a catch all when registering state or event listeners.
|
# Can be used to specify a catch all when registering state or event listeners.
|
||||||
MATCH_ALL = '*'
|
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
|
# For all entity's, this hold whether or not it should be hidden
|
||||||
ATTR_HIDDEN = "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 ####
|
# #### SERVICES ####
|
||||||
SERVICE_HOMEASSISTANT_STOP = "stop"
|
SERVICE_HOMEASSISTANT_STOP = "stop"
|
||||||
|
|
||||||
|
@ -26,6 +26,7 @@ from homeassistant.exceptions import (
|
|||||||
HomeAssistantError, InvalidEntityFormatError)
|
HomeAssistantError, InvalidEntityFormatError)
|
||||||
import homeassistant.util as util
|
import homeassistant.util as util
|
||||||
import homeassistant.util.dt as date_util
|
import homeassistant.util.dt as date_util
|
||||||
|
import homeassistant.util.location as location
|
||||||
import homeassistant.helpers.temperature as temp_helper
|
import homeassistant.helpers.temperature as temp_helper
|
||||||
from homeassistant.config import get_default_config_dir
|
from homeassistant.config import get_default_config_dir
|
||||||
|
|
||||||
@ -445,9 +446,8 @@ class StateMachine(object):
|
|||||||
|
|
||||||
domain_filter = domain_filter.lower()
|
domain_filter = domain_filter.lower()
|
||||||
|
|
||||||
return [state.entity_id for key, state
|
return [state.entity_id for state in self._states.values()
|
||||||
in self._states.items()
|
if state.domain == domain_filter]
|
||||||
if util.split_entity_id(key)[0] == domain_filter]
|
|
||||||
|
|
||||||
def all(self):
|
def all(self):
|
||||||
""" Returns a list of all states. """
|
""" Returns a list of all states. """
|
||||||
@ -524,6 +524,28 @@ class StateMachine(object):
|
|||||||
from_state, to_state)
|
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
|
# pylint: disable=too-few-public-methods
|
||||||
class ServiceCall(object):
|
class ServiceCall(object):
|
||||||
""" Represents a call to a service. """
|
""" Represents a call to a service. """
|
||||||
@ -558,20 +580,29 @@ class ServiceRegistry(object):
|
|||||||
def services(self):
|
def services(self):
|
||||||
""" Dict with per domain a list of available services. """
|
""" Dict with per domain a list of available services. """
|
||||||
with self._lock:
|
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}
|
for domain in self._services}
|
||||||
|
|
||||||
def has_service(self, domain, service):
|
def has_service(self, domain, service):
|
||||||
""" Returns True if specified service exists. """
|
""" Returns True if specified service exists. """
|
||||||
return service in self._services.get(domain, [])
|
return service in self._services.get(domain, [])
|
||||||
|
|
||||||
def register(self, domain, service, service_func):
|
def register(self, domain, service, service_func, description=None):
|
||||||
""" Register a service. """
|
"""
|
||||||
|
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:
|
with self._lock:
|
||||||
if domain in self._services:
|
if domain in self._services:
|
||||||
self._services[domain][service] = service_func
|
self._services[domain][service] = service_obj
|
||||||
else:
|
else:
|
||||||
self._services[domain] = {service: service_func}
|
self._services[domain] = {service: service_obj}
|
||||||
|
|
||||||
self._bus.fire(
|
self._bus.fire(
|
||||||
EVENT_SERVICE_REGISTERED,
|
EVENT_SERVICE_REGISTERED,
|
||||||
@ -676,6 +707,10 @@ class Config(object):
|
|||||||
# Directory that holds the configuration
|
# Directory that holds the configuration
|
||||||
self.config_dir = get_default_config_dir()
|
self.config_dir = get_default_config_dir()
|
||||||
|
|
||||||
|
def distance(self, lat, lon):
|
||||||
|
""" Calculate distance from Home Assistant in meters. """
|
||||||
|
return location.distance(self.latitude, self.longitude, lat, lon)
|
||||||
|
|
||||||
def path(self, *path):
|
def path(self, *path):
|
||||||
""" Returns path to the file within the config dir. """
|
""" Returns path to the file within the config dir. """
|
||||||
return os.path.join(self.config_dir, *path)
|
return os.path.join(self.config_dir, *path)
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
"""
|
"""
|
||||||
Helper methods for components within Home Assistant.
|
Helper methods for components within Home Assistant.
|
||||||
"""
|
"""
|
||||||
|
import re
|
||||||
|
|
||||||
from homeassistant.loader import get_component
|
from homeassistant.loader import get_component
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_ENTITY_ID, CONF_PLATFORM, DEVICE_DEFAULT_NAME)
|
ATTR_ENTITY_ID, CONF_PLATFORM, DEVICE_DEFAULT_NAME)
|
||||||
@ -73,7 +75,7 @@ def config_per_platform(config, domain, logger):
|
|||||||
config_key = domain
|
config_key = domain
|
||||||
found = 1
|
found = 1
|
||||||
|
|
||||||
while config_key in config:
|
for config_key in extract_domain_configs(config, domain):
|
||||||
platform_config = config[config_key]
|
platform_config = config[config_key]
|
||||||
if not isinstance(platform_config, list):
|
if not isinstance(platform_config, list):
|
||||||
platform_config = [platform_config]
|
platform_config = [platform_config]
|
||||||
@ -89,3 +91,9 @@ def config_per_platform(config, domain, logger):
|
|||||||
|
|
||||||
found += 1
|
found += 1
|
||||||
config_key = "{} {}".format(domain, found)
|
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,9 @@ import logging
|
|||||||
from homeassistant.core import State
|
from homeassistant.core import State
|
||||||
import homeassistant.util.dt as dt_util
|
import homeassistant.util.dt as dt_util
|
||||||
from homeassistant.const import (
|
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)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -55,7 +57,11 @@ def reproduce_state(hass, states, blocking=False):
|
|||||||
state.entity_id)
|
state.entity_id)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if state.state == STATE_ON:
|
if 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
|
service = SERVICE_TURN_ON
|
||||||
elif state.state == STATE_OFF:
|
elif state.state == STATE_OFF:
|
||||||
service = SERVICE_TURN_OFF
|
service = SERVICE_TURN_OFF
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
import collections
|
import collections
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
from vincenty import vincenty
|
||||||
|
|
||||||
|
|
||||||
LocationInfo = collections.namedtuple(
|
LocationInfo = collections.namedtuple(
|
||||||
@ -28,3 +29,8 @@ def detect_location_info():
|
|||||||
'BS', 'BZ', 'KY', 'PW', 'US', 'AS', 'VI')
|
'BS', 'BZ', 'KY', 'PW', 'US', 'AS', 'VI')
|
||||||
|
|
||||||
return LocationInfo(**data)
|
return LocationInfo(**data)
|
||||||
|
|
||||||
|
|
||||||
|
def distance(lat1, lon1, lat2, lon2):
|
||||||
|
""" Calculate the distance in meters between two points. """
|
||||||
|
return vincenty((lat1, lon1), (lat2, lon2)) * 1000
|
||||||
|
@ -3,6 +3,7 @@ requests>=2,<3
|
|||||||
pyyaml>=3.11,<4
|
pyyaml>=3.11,<4
|
||||||
pytz>=2015.4
|
pytz>=2015.4
|
||||||
pip>=7.0.0
|
pip>=7.0.0
|
||||||
|
vincenty==0.1.2
|
||||||
|
|
||||||
# Optional, needed for specific components
|
# Optional, needed for specific components
|
||||||
|
|
||||||
@ -13,7 +14,7 @@ astral==0.8.1
|
|||||||
phue==0.8
|
phue==0.8
|
||||||
|
|
||||||
# Limitlessled/Easybulb/Milight library (lights.limitlessled)
|
# Limitlessled/Easybulb/Milight library (lights.limitlessled)
|
||||||
ledcontroller==1.0.7
|
ledcontroller==1.1.0
|
||||||
|
|
||||||
# Chromecast bindings (media_player.cast)
|
# Chromecast bindings (media_player.cast)
|
||||||
pychromecast==0.6.12
|
pychromecast==0.6.12
|
||||||
@ -22,7 +23,7 @@ pychromecast==0.6.12
|
|||||||
pyuserinput==0.1.9
|
pyuserinput==0.1.9
|
||||||
|
|
||||||
# Tellstick bindings (*.tellstick)
|
# Tellstick bindings (*.tellstick)
|
||||||
tellcore-py==1.0.4
|
tellcore-py==1.1.2
|
||||||
|
|
||||||
# Nmap bindings (device_tracker.nmap)
|
# Nmap bindings (device_tracker.nmap)
|
||||||
python-nmap==0.4.3
|
python-nmap==0.4.3
|
||||||
@ -86,10 +87,10 @@ https://github.com/theolind/pymysensors/archive/35b87d880147a34107da0d40cb815d75
|
|||||||
pynetgear==0.3
|
pynetgear==0.3
|
||||||
|
|
||||||
# Netdisco (discovery)
|
# Netdisco (discovery)
|
||||||
netdisco==0.4
|
netdisco==0.4.2
|
||||||
|
|
||||||
# Wemo (switch.wemo)
|
# Wemo (switch.wemo)
|
||||||
pywemo==0.3
|
pywemo==0.3.1
|
||||||
|
|
||||||
# Wink (*.wink)
|
# Wink (*.wink)
|
||||||
https://github.com/balloob/python-wink/archive/c2b700e8ca866159566ecf5e644d9c297f69f257.zip#python-wink==0.1
|
https://github.com/balloob/python-wink/archive/c2b700e8ca866159566ecf5e644d9c297f69f257.zip#python-wink==0.1
|
||||||
@ -133,3 +134,6 @@ https://github.com/balloob/home-assistant-vera-api/archive/a8f823066ead6c7da6fb5
|
|||||||
|
|
||||||
# Sonos bindings (media_player.sonos)
|
# Sonos bindings (media_player.sonos)
|
||||||
SoCo==0.11.1
|
SoCo==0.11.1
|
||||||
|
|
||||||
|
# PlexAPI (media_player.plex)
|
||||||
|
https://github.com/adrienbrault/python-plexapi/archive/df2d0847e801d6d5cda920326d693cf75f304f1a.zip#python-plexapi==1.0.2
|
||||||
|
@ -3,6 +3,8 @@
|
|||||||
# script/cibuild: Setup environment for CI to run tests. This is primarily
|
# script/cibuild: Setup environment for CI to run tests. This is primarily
|
||||||
# designed to run on the continuous integration server.
|
# designed to run on the continuous integration server.
|
||||||
|
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
script/test coverage
|
script/test coverage
|
||||||
|
|
||||||
STATUS=$?
|
STATUS=$?
|
||||||
|
14
script/home-assistant@.service
Normal file
14
script/home-assistant@.service
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
# This is a simple service file for systems with systemd to tun HA as user.
|
||||||
|
#
|
||||||
|
[Unit]
|
||||||
|
Description=Home Assistant for %i
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=%i
|
||||||
|
WorkingDirectory=%h
|
||||||
|
ExecStart=/usr/bin/hass --config %h/.homeassistant/
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
@ -5,14 +5,15 @@ cd "$(dirname "$0")/.."
|
|||||||
echo "Checking style with flake8..."
|
echo "Checking style with flake8..."
|
||||||
flake8 --exclude www_static homeassistant
|
flake8 --exclude www_static homeassistant
|
||||||
|
|
||||||
STATUS=$?
|
FLAKE8_STATUS=$?
|
||||||
|
|
||||||
echo "Checking style with pylint..."
|
echo "Checking style with pylint..."
|
||||||
pylint homeassistant
|
pylint homeassistant
|
||||||
|
PYLINT_STATUS=$?
|
||||||
|
|
||||||
if [ $STATUS -eq 0 ]
|
if [ $FLAKE8_STATUS -eq 0 ]
|
||||||
then
|
then
|
||||||
exit $?
|
exit $PYLINT_STATUS
|
||||||
else
|
else
|
||||||
exit $STATUS
|
exit $FLAKE8_STATUS
|
||||||
fi
|
fi
|
||||||
|
21
script/release
Executable file
21
script/release
Executable file
@ -0,0 +1,21 @@
|
|||||||
|
# Pushes a new version to PyPi
|
||||||
|
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
|
head -n 3 homeassistant/const.py | tail -n 1 | grep dev
|
||||||
|
|
||||||
|
if [ $? -eq 0 ]
|
||||||
|
then
|
||||||
|
echo "Release version should not contain dev tag"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
CURRENT_BRANCH=`git rev-parse --abbrev-ref HEAD`
|
||||||
|
|
||||||
|
if [ "$CURRENT_BRANCH" != "master" ]
|
||||||
|
then
|
||||||
|
echo "You have to be on the master branch to release."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
python3 setup.py sdist bdist_wheel upload
|
10
script/test
10
script/test
@ -7,19 +7,21 @@ cd "$(dirname "$0")/.."
|
|||||||
|
|
||||||
script/lint
|
script/lint
|
||||||
|
|
||||||
STATUS=$?
|
LINT_STATUS=$?
|
||||||
|
|
||||||
echo "Running tests..."
|
echo "Running tests..."
|
||||||
|
|
||||||
if [ "$1" = "coverage" ]; then
|
if [ "$1" = "coverage" ]; then
|
||||||
py.test --cov --cov-report=
|
py.test --cov --cov-report=
|
||||||
|
TEST_STATUS=$?
|
||||||
else
|
else
|
||||||
py.test
|
py.test
|
||||||
|
TEST_STATUS=$?
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ $STATUS -eq 0 ]
|
if [ $LINT_STATUS -eq 0 ]
|
||||||
then
|
then
|
||||||
exit $?
|
exit $TEST_STATUS
|
||||||
else
|
else
|
||||||
exit $STATUS
|
exit $LINT_STATUS
|
||||||
fi
|
fi
|
||||||
|
1
setup.py
1
setup.py
@ -20,6 +20,7 @@ REQUIRES = [
|
|||||||
'pyyaml>=3.11,<4',
|
'pyyaml>=3.11,<4',
|
||||||
'pytz>=2015.4',
|
'pytz>=2015.4',
|
||||||
'pip>=7.0.0',
|
'pip>=7.0.0',
|
||||||
|
'vincenty==0.1.2'
|
||||||
]
|
]
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
|
@ -124,14 +124,17 @@ def mock_http_component(hass):
|
|||||||
hass.config.components.append('http')
|
hass.config.components.append('http')
|
||||||
|
|
||||||
|
|
||||||
def mock_mqtt_component(hass):
|
@mock.patch('homeassistant.components.mqtt.MQTT')
|
||||||
with mock.patch('homeassistant.components.mqtt.MQTT'):
|
@mock.patch('homeassistant.components.mqtt.MQTT.publish')
|
||||||
mqtt.setup(hass, {
|
def mock_mqtt_component(hass, mock_mqtt, mock_mqtt_publish):
|
||||||
mqtt.DOMAIN: {
|
mqtt.setup(hass, {
|
||||||
mqtt.CONF_BROKER: 'mock-broker',
|
mqtt.DOMAIN: {
|
||||||
}
|
mqtt.CONF_BROKER: 'mock-broker',
|
||||||
})
|
}
|
||||||
hass.config.components.append(mqtt.DOMAIN)
|
})
|
||||||
|
hass.config.components.append(mqtt.DOMAIN)
|
||||||
|
|
||||||
|
return mock_mqtt_publish
|
||||||
|
|
||||||
|
|
||||||
class MockHTTP(object):
|
class MockHTTP(object):
|
||||||
|
@ -11,7 +11,7 @@ import homeassistant.components.automation as automation
|
|||||||
from tests.common import mock_mqtt_component, fire_mqtt_message
|
from tests.common import mock_mqtt_component, fire_mqtt_message
|
||||||
|
|
||||||
|
|
||||||
class TestAutomationState(unittest.TestCase):
|
class TestAutomationMQTT(unittest.TestCase):
|
||||||
""" Test the event automation. """
|
""" Test the event automation. """
|
||||||
|
|
||||||
def setUp(self): # pylint: disable=invalid-name
|
def setUp(self): # pylint: disable=invalid-name
|
||||||
|
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))
|
@ -103,12 +103,12 @@ class TestComponentsDeviceTracker(unittest.TestCase):
|
|||||||
def test_reading_yaml_config(self):
|
def test_reading_yaml_config(self):
|
||||||
dev_id = 'test'
|
dev_id = 'test'
|
||||||
device = device_tracker.Device(
|
device = device_tracker.Device(
|
||||||
self.hass, timedelta(seconds=180), True, dev_id, 'AB:CD:EF:GH:IJ',
|
self.hass, timedelta(seconds=180), 0, True, dev_id,
|
||||||
'Test name', 'http://test.picture', True)
|
'AB:CD:EF:GH:IJ', 'Test name', 'http://test.picture', True)
|
||||||
device_tracker.update_config(self.yaml_devices, dev_id, device)
|
device_tracker.update_config(self.yaml_devices, dev_id, device)
|
||||||
self.assertTrue(device_tracker.setup(self.hass, {}))
|
self.assertTrue(device_tracker.setup(self.hass, {}))
|
||||||
config = device_tracker.load_config(self.yaml_devices, self.hass,
|
config = device_tracker.load_config(self.yaml_devices, self.hass,
|
||||||
device.consider_home)[0]
|
device.consider_home, 0)[0]
|
||||||
self.assertEqual(device.dev_id, config.dev_id)
|
self.assertEqual(device.dev_id, config.dev_id)
|
||||||
self.assertEqual(device.track, config.track)
|
self.assertEqual(device.track, config.track)
|
||||||
self.assertEqual(device.mac, config.mac)
|
self.assertEqual(device.mac, config.mac)
|
||||||
@ -126,7 +126,7 @@ class TestComponentsDeviceTracker(unittest.TestCase):
|
|||||||
self.assertTrue(device_tracker.setup(self.hass, {
|
self.assertTrue(device_tracker.setup(self.hass, {
|
||||||
device_tracker.DOMAIN: {CONF_PLATFORM: 'test'}}))
|
device_tracker.DOMAIN: {CONF_PLATFORM: 'test'}}))
|
||||||
config = device_tracker.load_config(self.yaml_devices, self.hass,
|
config = device_tracker.load_config(self.yaml_devices, self.hass,
|
||||||
timedelta(seconds=0))[0]
|
timedelta(seconds=0), 0)[0]
|
||||||
self.assertEqual('dev1', config.dev_id)
|
self.assertEqual('dev1', config.dev_id)
|
||||||
self.assertEqual(True, config.track)
|
self.assertEqual(True, config.track)
|
||||||
|
|
||||||
@ -176,7 +176,7 @@ class TestComponentsDeviceTracker(unittest.TestCase):
|
|||||||
picture = 'http://placehold.it/200x200'
|
picture = 'http://placehold.it/200x200'
|
||||||
|
|
||||||
device = device_tracker.Device(
|
device = device_tracker.Device(
|
||||||
self.hass, timedelta(seconds=180), True, dev_id, None,
|
self.hass, timedelta(seconds=180), 0, True, dev_id, None,
|
||||||
friendly_name, picture, away_hide=True)
|
friendly_name, picture, away_hide=True)
|
||||||
device_tracker.update_config(self.yaml_devices, dev_id, device)
|
device_tracker.update_config(self.yaml_devices, dev_id, device)
|
||||||
|
|
||||||
@ -191,7 +191,7 @@ class TestComponentsDeviceTracker(unittest.TestCase):
|
|||||||
dev_id = 'test_entity'
|
dev_id = 'test_entity'
|
||||||
entity_id = device_tracker.ENTITY_ID_FORMAT.format(dev_id)
|
entity_id = device_tracker.ENTITY_ID_FORMAT.format(dev_id)
|
||||||
device = device_tracker.Device(
|
device = device_tracker.Device(
|
||||||
self.hass, timedelta(seconds=180), True, dev_id, None,
|
self.hass, timedelta(seconds=180), 0, True, dev_id, None,
|
||||||
away_hide=True)
|
away_hide=True)
|
||||||
device_tracker.update_config(self.yaml_devices, dev_id, device)
|
device_tracker.update_config(self.yaml_devices, dev_id, device)
|
||||||
|
|
||||||
@ -208,7 +208,7 @@ class TestComponentsDeviceTracker(unittest.TestCase):
|
|||||||
dev_id = 'test_entity'
|
dev_id = 'test_entity'
|
||||||
entity_id = device_tracker.ENTITY_ID_FORMAT.format(dev_id)
|
entity_id = device_tracker.ENTITY_ID_FORMAT.format(dev_id)
|
||||||
device = device_tracker.Device(
|
device = device_tracker.Device(
|
||||||
self.hass, timedelta(seconds=180), True, dev_id, None,
|
self.hass, timedelta(seconds=180), 0, True, dev_id, None,
|
||||||
away_hide=True)
|
away_hide=True)
|
||||||
device_tracker.update_config(self.yaml_devices, dev_id, device)
|
device_tracker.update_config(self.yaml_devices, dev_id, device)
|
||||||
|
|
||||||
|
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)
|
@ -8,9 +8,8 @@ Tests component helpers.
|
|||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
import homeassistant.core as ha
|
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.const import STATE_ON, STATE_OFF, ATTR_ENTITY_ID
|
||||||
from homeassistant.helpers import extract_entity_ids
|
|
||||||
|
|
||||||
from tests.common import get_test_home_assistant
|
from tests.common import get_test_home_assistant
|
||||||
|
|
||||||
@ -39,10 +38,22 @@ class TestComponentsCore(unittest.TestCase):
|
|||||||
{ATTR_ENTITY_ID: 'light.Bowl'})
|
{ATTR_ENTITY_ID: 'light.Bowl'})
|
||||||
|
|
||||||
self.assertEqual(['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',
|
call = ha.ServiceCall('light', 'turn_on',
|
||||||
{ATTR_ENTITY_ID: 'group.test'})
|
{ATTR_ENTITY_ID: 'group.test'})
|
||||||
|
|
||||||
self.assertEqual(['light.ceiling', 'light.kitchen'],
|
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):
|
def test_services(self):
|
||||||
expected = {
|
expected = {
|
||||||
'test_domain': ['test_service']
|
'test_domain': {'test_service': {'description': '', 'fields': {}}}
|
||||||
}
|
}
|
||||||
self.assertEqual(expected, self.services.services)
|
self.assertEqual(expected, self.services.services)
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user