merged with current dev

This commit is contained in:
Malte Deiseroth 2015-09-22 12:11:57 +02:00
commit e6e3b37a62
121 changed files with 6717 additions and 1378 deletions

View File

@ -46,9 +46,11 @@ omit =
homeassistant/components/light/limitlessled.py
homeassistant/components/media_player/cast.py
homeassistant/components/media_player/denon.py
homeassistant/components/media_player/itunes.py
homeassistant/components/media_player/kodi.py
homeassistant/components/media_player/mpd.py
homeassistant/components/media_player/squeezebox.py
homeassistant/components/media_player/sonos.py
homeassistant/components/notify/file.py
homeassistant/components/notify/instapush.py
homeassistant/components/notify/nma.py
@ -60,9 +62,11 @@ omit =
homeassistant/components/notify/xmpp.py
homeassistant/components/sensor/arest.py
homeassistant/components/sensor/bitcoin.py
homeassistant/components/sensor/command_sensor.py
homeassistant/components/sensor/dht.py
homeassistant/components/sensor/efergy.py
homeassistant/components/sensor/forecast.py
homeassistant/components/sensor/glances.py
homeassistant/components/sensor/mysensors.py
homeassistant/components/sensor/openweathermap.py
homeassistant/components/sensor/rfxtrx.py
@ -73,6 +77,7 @@ omit =
homeassistant/components/sensor/temper.py
homeassistant/components/sensor/time_date.py
homeassistant/components/sensor/transmission.py
homeassistant/components/switch/arest.py
homeassistant/components/switch/command_switch.py
homeassistant/components/switch/edimax.py
homeassistant/components/switch/hikvisioncam.py

7
.gitignore vendored
View File

@ -15,10 +15,6 @@ tests/config/home-assistant.log
*.sublime-project
*.sublime-workspace
# Hide code validator output
pep8.txt
pylint.txt
# Hide some OS X stuff
.DS_Store
.AppleDouble
@ -30,6 +26,9 @@ Icon
.idea
# pytest
.cache
# GITHUB Proposed Python stuff:
*.py[cod]

View File

@ -3,11 +3,6 @@ language: python
python:
- "3.4"
install:
- pip install -r requirements_all.txt
- pip install flake8 pylint coveralls
- script/bootstrap_server
script:
- flake8 homeassistant
- pylint homeassistant
- coverage run -m unittest discover tests
after_success:
- coveralls
- script/cibuild

View File

@ -18,7 +18,7 @@ For help on building your component, please see the [developer documentation](ht
After you finish adding support for your device:
- 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.
- 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`.

View File

@ -18,7 +18,7 @@ Examples of devices it can interface it:
* Monitoring connected devices to a wireless router: [OpenWrt](https://openwrt.org/), [Tomato](http://www.polarcloud.com/tomato), [Netgear](http://netgear.com), [DD-WRT](http://www.dd-wrt.com/site/index), [TPLink](http://www.tp-link.us/), and [ASUSWRT](http://event.asus.com/2013/nw/ASUSWRT/)
* [Philips Hue](http://meethue.com) lights, [WeMo](http://www.belkin.com/us/Products/home-automation/c/wemo-home-automation/) switches, [Edimax](http://www.edimax.com/) switches, [Efergy](https://efergy.com) energy monitoring, RFXtrx sensors, and [Tellstick](http://www.telldus.se/products/tellstick) devices and sensors
* [Google Chromecasts](http://www.google.com/intl/en/chrome/devices/chromecast), [Music Player Daemon](http://www.musicpd.org/), [Logitech Squeezebox](https://en.wikipedia.org/wiki/Squeezebox_%28network_music_player%29), and [Kodi (XBMC)](http://kodi.tv/)
* [Google Chromecasts](http://www.google.com/intl/en/chrome/devices/chromecast), [Music Player Daemon](http://www.musicpd.org/), [Logitech Squeezebox](https://en.wikipedia.org/wiki/Squeezebox_%28network_music_player%29), [Kodi (XBMC)](http://kodi.tv/), and iTunes (by way of [itunes-api](https://github.com/maddox/itunes-api))
* Support for [ISY994](https://www.universal-devices.com/residential/isy994i-series/) (Insteon and X10 devices), [Z-Wave](http://www.z-wave.com/), [Nest Thermostats](https://nest.com/), [Arduino](https://www.arduino.cc/), [Raspberry Pi](https://www.raspberrypi.org/), and [Modbus](http://www.modbus.org/)
* Integrate data from the [Bitcoin](https://bitcoin.org) network, meteorological data from [OpenWeatherMap](http://openweathermap.org/) and [Forecast.io](https://forecast.io/), [Transmission](http://www.transmissionbt.com/), or [SABnzbd](http://sabnzbd.org).
* [See full list of supported devices](https://home-assistant.io/components/)

View File

@ -95,6 +95,18 @@ def get_arguments():
type=int,
default=None,
help='Enables daily log rotation and keeps up to the specified days')
parser.add_argument(
'--install-osx',
action='store_true',
help='Installs as a service on OS X and loads on boot.')
parser.add_argument(
'--uninstall-osx',
action='store_true',
help='Uninstalls from OS X.')
parser.add_argument(
'--restart-osx',
action='store_true',
help='Restarts on OS X.')
if os.name != "nt":
parser.add_argument(
'--daemon',
@ -152,6 +164,46 @@ def write_pid(pid_file):
sys.exit(1)
def install_osx():
""" Setup to run via launchd on OS X """
with os.popen('which hass') as inp:
hass_path = inp.read().strip()
with os.popen('whoami') as inp:
user = inp.read().strip()
cwd = os.path.dirname(__file__)
template_path = os.path.join(cwd, 'startup', 'launchd.plist')
with open(template_path, 'r', encoding='utf-8') as inp:
plist = inp.read()
plist = plist.replace("$HASS_PATH$", hass_path)
plist = plist.replace("$USER$", user)
path = os.path.expanduser("~/Library/LaunchAgents/org.homeassistant.plist")
try:
with open(path, 'w', encoding='utf-8') as outp:
outp.write(plist)
except IOError as err:
print('Unable to write to ' + path, err)
return
os.popen('launchctl load -w -F ' + path)
print("Home Assistant has been installed. \
Open it here: http://localhost:8123")
def uninstall_osx():
""" Unload from launchd on OS X """
path = os.path.expanduser("~/Library/LaunchAgents/org.homeassistant.plist")
os.popen('launchctl unload ' + path)
print("Home Assistant has been uninstalled.")
def main():
""" Starts Home Assistant. """
validate_python()
@ -161,6 +213,18 @@ def main():
config_dir = os.path.join(os.getcwd(), args.config)
ensure_config_path(config_dir)
# os x launchd functions
if args.install_osx:
install_osx()
return
if args.uninstall_osx:
uninstall_osx()
return
if args.restart_osx:
uninstall_osx()
install_osx()
return
# daemon functions
if args.pid_file:
check_pid(args.pid_file)

View File

@ -123,6 +123,7 @@ def prepare_setup_platform(hass, config, domain, platform_name):
# Not found
if platform is None:
_LOGGER.error('Unable to find platform %s', platform_path)
return None
# Already loaded
@ -296,11 +297,15 @@ def process_ha_core_config(hass, config):
else:
_LOGGER.error('Received invalid time zone %s', time_zone_str)
for key, attr in ((CONF_LATITUDE, 'latitude'),
(CONF_LONGITUDE, 'longitude'),
(CONF_NAME, 'location_name')):
for key, attr, typ in ((CONF_LATITUDE, 'latitude', float),
(CONF_LONGITUDE, 'longitude', float),
(CONF_NAME, 'location_name', str)):
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))

View File

@ -0,0 +1,108 @@
"""
homeassistant.components.alarm_control_panel
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Component to interface with a alarm control panel.
"""
import logging
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.components import verisure
from homeassistant.const import (
ATTR_ENTITY_ID,
SERVICE_ALARM_DISARM, SERVICE_ALARM_ARM_HOME, SERVICE_ALARM_ARM_AWAY)
DOMAIN = 'alarm_control_panel'
DEPENDENCIES = []
SCAN_INTERVAL = 30
ENTITY_ID_FORMAT = DOMAIN + '.{}'
# Maps discovered services to their platforms
DISCOVERY_PLATFORMS = {
verisure.DISCOVER_SENSORS: 'verisure'
}
SERVICE_TO_METHOD = {
SERVICE_ALARM_DISARM: 'alarm_disarm',
SERVICE_ALARM_ARM_HOME: 'alarm_arm_home',
SERVICE_ALARM_ARM_AWAY: 'alarm_arm_away',
}
ATTR_CODE = 'code'
ATTR_TO_PROPERTY = [
ATTR_CODE,
]
def setup(hass, config):
""" Track states and offer events for sensors. """
component = EntityComponent(
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL,
DISCOVERY_PLATFORMS)
component.setup(config)
def alarm_service_handler(service):
""" Maps services to methods on Alarm. """
target_alarms = component.extract_from_service(service)
if ATTR_CODE not in service.data:
return
code = service.data[ATTR_CODE]
method = SERVICE_TO_METHOD[service.service]
for alarm in target_alarms:
getattr(alarm, method)(code)
for service in SERVICE_TO_METHOD:
hass.services.register(DOMAIN, service, alarm_service_handler)
return True
def alarm_disarm(hass, code, entity_id=None):
""" Send the alarm the command for disarm. """
data = {ATTR_CODE: code}
if entity_id:
data[ATTR_ENTITY_ID] = entity_id
hass.services.call(DOMAIN, SERVICE_ALARM_DISARM, data)
def alarm_arm_home(hass, code, entity_id=None):
""" Send the alarm the command for arm home. """
data = {ATTR_CODE: code}
if entity_id:
data[ATTR_ENTITY_ID] = entity_id
hass.services.call(DOMAIN, SERVICE_ALARM_ARM_HOME, data)
def alarm_arm_away(hass, code, entity_id=None):
""" Send the alarm the command for arm away. """
data = {ATTR_CODE: code}
if entity_id:
data[ATTR_ENTITY_ID] = entity_id
hass.services.call(DOMAIN, SERVICE_ALARM_ARM_AWAY, data)
class AlarmControlPanel(Entity):
""" ABC for alarm control devices. """
def alarm_disarm(self, code):
""" Send disarm command. """
raise NotImplementedError()
def alarm_arm_home(self, code):
""" Send arm home command. """
raise NotImplementedError()
def alarm_arm_away(self, code):
""" Send arm away command. """
raise NotImplementedError()

View File

@ -0,0 +1,88 @@
"""
homeassistant.components.alarm_control_panel.verisure
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Interfaces with Verisure alarm control panel.
"""
import logging
import homeassistant.components.verisure as verisure
import homeassistant.components.alarm_control_panel as alarm
from homeassistant.const import (
STATE_UNKNOWN,
STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_AWAY)
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None):
""" Sets up the Verisure platform. """
if not verisure.MY_PAGES:
_LOGGER.error('A connection has not been made to Verisure mypages.')
return False
alarms = []
alarms.extend([
VerisureAlarm(value)
for value in verisure.get_alarm_status().values()
if verisure.SHOW_ALARM
])
add_devices(alarms)
class VerisureAlarm(alarm.AlarmControlPanel):
""" represents a Verisure alarm status within home assistant. """
def __init__(self, alarm_status):
self._id = alarm_status.id
self._device = verisure.MY_PAGES.DEVICE_ALARM
self._state = STATE_UNKNOWN
@property
def name(self):
""" Returns the name of the device. """
return 'Alarm {}'.format(self._id)
@property
def state(self):
""" Returns the state of the device. """
return self._state
def update(self):
''' update alarm status '''
verisure.update()
if verisure.STATUS[self._device][self._id].status == 'unarmed':
self._state = STATE_ALARM_DISARMED
elif verisure.STATUS[self._device][self._id].status == 'armedhome':
self._state = STATE_ALARM_ARMED_HOME
elif verisure.STATUS[self._device][self._id].status == 'armedaway':
self._state = STATE_ALARM_ARMED_AWAY
elif verisure.STATUS[self._device][self._id].status != 'pending':
_LOGGER.error(
'Unknown alarm state %s',
verisure.STATUS[self._device][self._id].status)
def alarm_disarm(self, code):
""" Send disarm command. """
verisure.MY_PAGES.set_alarm_status(
code,
verisure.MY_PAGES.ALARM_DISARMED)
_LOGGER.warning('disarming')
def alarm_arm_home(self, code):
""" Send arm home command. """
verisure.MY_PAGES.set_alarm_status(
code,
verisure.MY_PAGES.ALARM_ARMED_HOME)
_LOGGER.warning('arming home')
def alarm_arm_away(self, code):
""" Send arm away command. """
verisure.MY_PAGES.set_alarm_status(
code,
verisure.MY_PAGES.ALARM_ARMED_AWAY)
_LOGGER.warning('arming away')

View File

@ -7,68 +7,217 @@ Allows to setup simple automation rules via the config file.
import logging
from homeassistant.bootstrap import prepare_setup_platform
from homeassistant.helpers import config_per_platform
from homeassistant.util import split_entity_id
from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.const import ATTR_ENTITY_ID, CONF_PLATFORM
from homeassistant.components import logbook
DOMAIN = "automation"
DOMAIN = 'automation'
DEPENDENCIES = ["group"]
DEPENDENCIES = ['group']
CONF_ALIAS = "alias"
CONF_SERVICE = "execute_service"
CONF_SERVICE_ENTITY_ID = "service_entity_id"
CONF_SERVICE_DATA = "service_data"
CONF_ALIAS = 'alias'
CONF_SERVICE = 'service'
CONF_SERVICE_ENTITY_ID = 'entity_id'
CONF_SERVICE_DATA = 'data'
CONF_CONDITION = 'condition'
CONF_ACTION = 'action'
CONF_TRIGGER = 'trigger'
CONF_CONDITION_TYPE = 'condition_type'
CONDITION_USE_TRIGGER_VALUES = 'use_trigger_values'
CONDITION_TYPE_AND = 'and'
CONDITION_TYPE_OR = 'or'
DEFAULT_CONDITION_TYPE = CONDITION_TYPE_AND
_LOGGER = logging.getLogger(__name__)
def setup(hass, config):
""" Sets up automation. """
success = False
config_key = DOMAIN
found = 1
for p_type, p_config in config_per_platform(config, DOMAIN, _LOGGER):
platform = prepare_setup_platform(hass, config, DOMAIN, p_type)
while config_key in config:
# check for one block syntax
if isinstance(config[config_key], dict):
config_block = _migrate_old_config(config[config_key])
name = config_block.get(CONF_ALIAS, config_key)
_setup_automation(hass, config_block, name, config)
if platform is None:
_LOGGER.error("Unknown automation platform specified: %s", p_type)
continue
# check for multiple block syntax
elif isinstance(config[config_key], list):
for list_no, config_block in enumerate(config[config_key]):
name = config_block.get(CONF_ALIAS,
"{}, {}".format(config_key, list_no))
_setup_automation(hass, config_block, name, config)
if platform.register(hass, p_config, _get_action(hass, p_config)):
_LOGGER.info(
"Initialized %s rule %s", p_type, p_config.get(CONF_ALIAS, ""))
success = True
# any scalar value is incorrect
else:
_LOGGER.error(
"Error setting up rule %s", p_config.get(CONF_ALIAS, ""))
_LOGGER.error('Error in config in section %s.', config_key)
return success
found += 1
config_key = "{} {}".format(DOMAIN, found)
return True
def _get_action(hass, config):
def _setup_automation(hass, config_block, name, config):
""" Setup one instance of automation """
action = _get_action(hass, config_block.get(CONF_ACTION, {}), name)
if action is None:
return False
if CONF_CONDITION in config_block or CONF_CONDITION_TYPE in config_block:
action = _process_if(hass, config, config_block, action)
if action is None:
return False
_process_trigger(hass, config, config_block.get(CONF_TRIGGER, []), name,
action)
return True
def _get_action(hass, config, name):
""" Return an action based on a config. """
if CONF_SERVICE not in config:
_LOGGER.error('Error setting up %s, no action specified.', name)
return None
def action():
""" Action to be executed. """
_LOGGER.info("Executing rule %s", config.get(CONF_ALIAS, ""))
_LOGGER.info('Executing %s', name)
logbook.log_entry(hass, name, 'has been triggered', DOMAIN)
if CONF_SERVICE in config:
domain, service = split_entity_id(config[CONF_SERVICE])
domain, service = split_entity_id(config[CONF_SERVICE])
service_data = config.get(CONF_SERVICE_DATA, {})
service_data = config.get(CONF_SERVICE_DATA, {})
if not isinstance(service_data, dict):
_LOGGER.error("%s should be a dictionary", CONF_SERVICE_DATA)
service_data = {}
if not isinstance(service_data, dict):
_LOGGER.error("%s should be a dictionary", CONF_SERVICE_DATA)
service_data = {}
if CONF_SERVICE_ENTITY_ID in config:
try:
service_data[ATTR_ENTITY_ID] = \
config[CONF_SERVICE_ENTITY_ID].split(",")
except AttributeError:
service_data[ATTR_ENTITY_ID] = \
config[CONF_SERVICE_ENTITY_ID]
if CONF_SERVICE_ENTITY_ID in config:
try:
service_data[ATTR_ENTITY_ID] = \
config[CONF_SERVICE_ENTITY_ID].split(",")
except AttributeError:
service_data[ATTR_ENTITY_ID] = \
config[CONF_SERVICE_ENTITY_ID]
hass.services.call(domain, service, service_data)
hass.services.call(domain, service, service_data)
return action
def _migrate_old_config(config):
""" Migrate old config to new. """
if CONF_PLATFORM not in config:
return config
_LOGGER.warning(
'You are using an old configuration format. Please upgrade: '
'https://home-assistant.io/components/automation.html')
new_conf = {
CONF_TRIGGER: dict(config),
CONF_CONDITION: config.get('if', []),
CONF_ACTION: dict(config),
}
for cat, key, new_key in (('trigger', 'mqtt_topic', 'topic'),
('trigger', 'mqtt_payload', 'payload'),
('trigger', 'state_entity_id', 'entity_id'),
('trigger', 'state_before', 'before'),
('trigger', 'state_after', 'after'),
('trigger', 'state_to', 'to'),
('trigger', 'state_from', 'from'),
('trigger', 'state_hours', 'hours'),
('trigger', 'state_minutes', 'minutes'),
('trigger', 'state_seconds', 'seconds'),
('action', 'execute_service', 'service'),
('action', 'service_entity_id', 'entity_id'),
('action', 'service_data', 'data')):
if key in new_conf[cat]:
new_conf[cat][new_key] = new_conf[cat].pop(key)
return new_conf
def _process_if(hass, config, p_config, action):
""" Processes if checks. """
cond_type = p_config.get(CONF_CONDITION_TYPE,
DEFAULT_CONDITION_TYPE).lower()
if_configs = p_config.get(CONF_CONDITION)
use_trigger = if_configs == CONDITION_USE_TRIGGER_VALUES
if use_trigger:
if_configs = p_config[CONF_TRIGGER]
if isinstance(if_configs, dict):
if_configs = [if_configs]
checks = []
for if_config in if_configs:
platform = _resolve_platform('if_action', hass, config,
if_config.get(CONF_PLATFORM))
if platform is None:
continue
check = platform.if_action(hass, if_config)
# Invalid conditions are allowed if we base it on trigger
if check is None and not use_trigger:
return None
checks.append(check)
if cond_type == CONDITION_TYPE_AND:
def if_action():
""" AND all conditions. """
if all(check() for check in checks):
action()
else:
def if_action():
""" OR all conditions. """
if any(check() for check in checks):
action()
return if_action
def _process_trigger(hass, config, trigger_configs, name, action):
""" Setup triggers. """
if isinstance(trigger_configs, dict):
trigger_configs = [trigger_configs]
for conf in trigger_configs:
platform = _resolve_platform('trigger', hass, config,
conf.get(CONF_PLATFORM))
if platform is None:
continue
if platform.trigger(hass, conf, action):
_LOGGER.info("Initialized rule %s", name)
else:
_LOGGER.error("Error setting up rule %s", name)
def _resolve_platform(method, hass, config, platform):
""" Find automation platform. """
if platform is None:
return None
platform = prepare_setup_platform(hass, config, DOMAIN, platform)
if platform is None or not hasattr(platform, method):
_LOGGER.error("Unknown automation platform specified for %s: %s",
method, platform)
return None
return platform

View File

@ -12,7 +12,7 @@ CONF_EVENT_DATA = "event_data"
_LOGGER = logging.getLogger(__name__)
def register(hass, config, action):
def trigger(hass, config, action):
""" Listen for events based on config. """
event_type = config.get(CONF_EVENT_TYPE)
@ -20,11 +20,12 @@ def register(hass, config, action):
_LOGGER.error("Missing configuration key %s", CONF_EVENT_TYPE)
return False
event_data = config.get(CONF_EVENT_DATA, {})
event_data = config.get(CONF_EVENT_DATA)
def handle_event(event):
""" Listens for events and calls the action when data matches. """
if event_data == event.data:
if not event_data or all(val == event.data.get(key) for key, val
in event_data.items()):
action()
hass.bus.listen(event_type, handle_event)

View File

@ -10,11 +10,11 @@ import homeassistant.components.mqtt as mqtt
DEPENDENCIES = ['mqtt']
CONF_TOPIC = 'mqtt_topic'
CONF_PAYLOAD = 'mqtt_payload'
CONF_TOPIC = 'topic'
CONF_PAYLOAD = 'payload'
def register(hass, config, action):
def trigger(hass, config, action):
""" Listen for state changes based on `config`. """
topic = config.get(CONF_TOPIC)
payload = config.get(CONF_PAYLOAD)

View File

@ -0,0 +1,91 @@
"""
homeassistant.components.automation.numeric_state
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Offers numeric state listening automation rules.
"""
import logging
from homeassistant.helpers.event import track_state_change
CONF_ENTITY_ID = "entity_id"
CONF_BELOW = "below"
CONF_ABOVE = "above"
_LOGGER = logging.getLogger(__name__)
def trigger(hass, config, action):
""" Listen for state changes based on `config`. """
entity_id = config.get(CONF_ENTITY_ID)
if entity_id is None:
_LOGGER.error("Missing configuration key %s", CONF_ENTITY_ID)
return False
below = config.get(CONF_BELOW)
above = config.get(CONF_ABOVE)
if below is None and above is None:
_LOGGER.error("Missing configuration key."
" One of %s or %s is required",
CONF_BELOW, CONF_ABOVE)
return False
# pylint: disable=unused-argument
def state_automation_listener(entity, from_s, to_s):
""" Listens for state changes and calls action. """
# Fire action if we go from outside range into range
if _in_range(to_s.state, above, below) and \
(from_s is None or not _in_range(from_s.state, above, below)):
action()
track_state_change(
hass, entity_id, state_automation_listener)
return True
def if_action(hass, config):
""" Wraps action method with state based condition. """
entity_id = config.get(CONF_ENTITY_ID)
if entity_id is None:
_LOGGER.error("Missing configuration key %s", CONF_ENTITY_ID)
return None
below = config.get(CONF_BELOW)
above = config.get(CONF_ABOVE)
if below is None and above is None:
_LOGGER.error("Missing configuration key."
" One of %s or %s is required",
CONF_BELOW, CONF_ABOVE)
return None
def if_numeric_state():
""" Test numeric state condition. """
state = hass.states.get(entity_id)
return state is not None and _in_range(state.state, above, below)
return if_numeric_state
def _in_range(value, range_start, range_end):
""" Checks if value is inside the range """
try:
value = float(value)
except ValueError:
_LOGGER.warn("Missing value in numeric check")
return False
if range_start is not None and range_end is not None:
return float(range_start) <= value < float(range_end)
elif range_end is not None:
return value < float(range_end)
else:
return float(range_start) <= value

View File

@ -10,22 +10,23 @@ from homeassistant.helpers.event import track_state_change
from homeassistant.const import MATCH_ALL
CONF_ENTITY_ID = "state_entity_id"
CONF_FROM = "state_from"
CONF_TO = "state_to"
CONF_ENTITY_ID = "entity_id"
CONF_FROM = "from"
CONF_TO = "to"
CONF_STATE = "state"
def register(hass, config, action):
def trigger(hass, config, action):
""" Listen for state changes based on `config`. """
entity_id = config.get(CONF_ENTITY_ID)
if entity_id is None:
logging.getLogger(__name__).error(
"Missing configuration key %s", CONF_ENTITY_ID)
"Missing trigger configuration key %s", CONF_ENTITY_ID)
return False
from_state = config.get(CONF_FROM, MATCH_ALL)
to_state = config.get(CONF_TO, MATCH_ALL)
to_state = config.get(CONF_TO) or config.get(CONF_STATE) or MATCH_ALL
def state_automation_listener(entity, from_s, to_s):
""" Listens for state changes and calls action. """
@ -35,3 +36,23 @@ def register(hass, config, action):
hass, entity_id, state_automation_listener, from_state, to_state)
return True
def if_action(hass, config):
""" Wraps action method with state based condition. """
entity_id = config.get(CONF_ENTITY_ID)
state = config.get(CONF_STATE)
if entity_id is None or state is None:
logging.getLogger(__name__).error(
"Missing if-condition configuration key %s or %s", CONF_ENTITY_ID,
CONF_STATE)
return None
state = str(state)
def if_state():
""" Test if condition. """
return hass.states.is_state(entity_id, state)
return if_state

View File

@ -0,0 +1,103 @@
"""
homeassistant.components.automation.sun
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Offers sun based automation rules.
"""
import logging
from datetime import timedelta
from homeassistant.components import sun
from homeassistant.helpers.event import track_point_in_utc_time
import homeassistant.util.dt as dt_util
DEPENDENCIES = ['sun']
CONF_OFFSET = 'offset'
CONF_EVENT = 'event'
EVENT_SUNSET = 'sunset'
EVENT_SUNRISE = 'sunrise'
_LOGGER = logging.getLogger(__name__)
def trigger(hass, config, action):
""" Listen for events based on config. """
event = config.get(CONF_EVENT)
if event is None:
_LOGGER.error("Missing configuration key %s", CONF_EVENT)
return False
event = event.lower()
if event not in (EVENT_SUNRISE, EVENT_SUNSET):
_LOGGER.error("Invalid value for %s: %s", CONF_EVENT, event)
return False
if CONF_OFFSET in config:
raw_offset = config.get(CONF_OFFSET)
negative_offset = False
if raw_offset.startswith('-'):
negative_offset = True
raw_offset = raw_offset[1:]
try:
(hour, minute, second) = [int(x) for x in raw_offset.split(':')]
except ValueError:
_LOGGER.error('Could not parse offset %s', raw_offset)
return False
offset = timedelta(hours=hour, minutes=minute, seconds=second)
if negative_offset:
offset *= -1
else:
offset = timedelta(0)
# Do something to call action
if event == EVENT_SUNRISE:
trigger_sunrise(hass, action, offset)
else:
trigger_sunset(hass, action, offset)
return True
def trigger_sunrise(hass, action, offset):
""" Trigger action at next sun rise. """
def next_rise():
""" Returns next sunrise. """
next_time = sun.next_rising_utc(hass) + offset
while next_time < dt_util.utcnow():
next_time = next_time + timedelta(days=1)
return next_time
def sunrise_automation_listener(now):
""" Called when it's time for action. """
track_point_in_utc_time(hass, sunrise_automation_listener, next_rise())
action()
track_point_in_utc_time(hass, sunrise_automation_listener, next_rise())
def trigger_sunset(hass, action, offset):
""" Trigger action at next sun set. """
def next_set():
""" Returns next sunrise. """
next_time = sun.next_setting_utc(hass) + offset
while next_time < dt_util.utcnow():
next_time = next_time + timedelta(days=1)
return next_time
def sunset_automation_listener(now):
""" Called when it's time for action. """
track_point_in_utc_time(hass, sunset_automation_listener, next_set())
action()
track_point_in_utc_time(hass, sunset_automation_listener, next_set())

View File

@ -4,19 +4,41 @@ homeassistant.components.automation.time
Offers time listening automation rules.
"""
import logging
from homeassistant.util import convert
import homeassistant.util.dt as dt_util
from homeassistant.helpers.event import track_time_change
CONF_HOURS = "time_hours"
CONF_MINUTES = "time_minutes"
CONF_SECONDS = "time_seconds"
CONF_HOURS = "hours"
CONF_MINUTES = "minutes"
CONF_SECONDS = "seconds"
CONF_BEFORE = "before"
CONF_AFTER = "after"
CONF_WEEKDAY = "weekday"
WEEKDAYS = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun']
_LOGGER = logging.getLogger(__name__)
def register(hass, config, action):
def trigger(hass, config, action):
""" Listen for state changes based on `config`. """
hours = convert(config.get(CONF_HOURS), int)
minutes = convert(config.get(CONF_MINUTES), int)
seconds = convert(config.get(CONF_SECONDS), int)
if CONF_AFTER in config:
after = dt_util.parse_time_str(config[CONF_AFTER])
if after is None:
_error_time(config[CONF_AFTER], CONF_AFTER)
return False
hours, minutes, seconds = after.hour, after.minute, after.second
elif (CONF_HOURS in config or CONF_MINUTES in config
or CONF_SECONDS in config):
hours = convert(config.get(CONF_HOURS), int)
minutes = convert(config.get(CONF_MINUTES), int)
seconds = convert(config.get(CONF_SECONDS), int)
else:
_LOGGER.error('One of %s, %s, %s OR %s needs to be specified',
CONF_HOURS, CONF_MINUTES, CONF_SECONDS, CONF_AFTER)
return False
def time_automation_listener(now):
""" Listens for time changes and calls action. """
@ -26,3 +48,58 @@ def register(hass, config, action):
hour=hours, minute=minutes, second=seconds)
return True
def if_action(hass, config):
""" Wraps action method with time based condition. """
before = config.get(CONF_BEFORE)
after = config.get(CONF_AFTER)
weekday = config.get(CONF_WEEKDAY)
if before is None and after is None and weekday is None:
logging.getLogger(__name__).error(
"Missing if-condition configuration key %s, %s or %s",
CONF_BEFORE, CONF_AFTER, CONF_WEEKDAY)
return None
if before is not None:
before = dt_util.parse_time_str(before)
if before is None:
_error_time(before, CONF_BEFORE)
return None
if after is not None:
after = dt_util.parse_time_str(after)
if after is None:
_error_time(after, CONF_AFTER)
return None
def time_if():
""" Validate time based if-condition """
now = dt_util.now()
if before is not None and now > now.replace(hour=before.hour,
minute=before.minute):
return False
if after is not None and now < now.replace(hour=after.hour,
minute=after.minute):
return False
if weekday is not None:
now_weekday = WEEKDAYS[now.weekday()]
if isinstance(weekday, str) and weekday != now_weekday or \
now_weekday not in weekday:
return False
return True
return time_if
def _error_time(value, key):
""" Helper method to print error. """
_LOGGER.error(
"Received invalid value for '%s': %s", key, value)
if isinstance(value, int):
_LOGGER.error('Make sure you wrap time values in quotes')

View File

@ -0,0 +1,105 @@
"""
homeassistant.components.camera.foscam
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
This component provides basic support for Foscam IP cameras.
As part of the basic support the following features will be provided:
-MJPEG video streaming
To use this component, add the following to your configuration.yaml file.
camera:
platform: foscam
name: Door Camera
ip: 192.168.0.123
port: 88
username: YOUR_USERNAME
password: YOUR_PASSWORD
Variables:
ip
*Required
The IP address of your Foscam device.
username
*Required
The username of a visitor or operator of your camera. Oddly admin accounts
don't seem to have access to take snapshots.
password
*Required
The password for accessing your camera.
name
*Optional
This parameter allows you to override the name of your camera in homeassistant.
port
*Optional
The port that the camera is running on. The default is 88.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/camera.foscam.html
"""
import logging
from homeassistant.helpers import validate_config
from homeassistant.components.camera import DOMAIN
from homeassistant.components.camera import Camera
import requests
import re
_LOGGER = logging.getLogger(__name__)
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
""" Adds a Foscam IP Camera. """
if not validate_config({DOMAIN: config},
{DOMAIN: ['username', 'password', 'ip']}, _LOGGER):
return None
add_devices_callback([FoscamCamera(config)])
# pylint: disable=too-many-instance-attributes
class FoscamCamera(Camera):
""" An implementation of a Foscam IP camera. """
def __init__(self, device_info):
super(FoscamCamera, self).__init__()
ip_address = device_info.get('ip')
port = device_info.get('port', 88)
self._base_url = 'http://' + ip_address + ':' + str(port) + '/'
self._username = device_info.get('username')
self._password = device_info.get('password')
self._snap_picture_url = self._base_url \
+ 'cgi-bin/CGIProxy.fcgi?cmd=snapPicture&usr=' \
+ self._username + '&pwd=' + self._password
self._name = device_info.get('name', 'Foscam Camera')
_LOGGER.info('Using the following URL for %s: %s',
self._name, self._snap_picture_url)
def camera_image(self):
""" Return a still image reponse from the camera. """
# send the request to snap a picture
response = requests.get(self._snap_picture_url)
# parse the response to find the image file name
pattern = re.compile('src="[.][.]/(.*[.]jpg)"')
filename = pattern.search(response.content.decode("utf-8")).group(1)
# send request for the image
response = requests.get(self._base_url + filename)
return response.content
@property
def name(self):
""" Return the name of this device. """
return self._name

View File

@ -17,7 +17,7 @@ DOMAIN = "demo"
DEPENDENCIES = ['introduction', 'conversation']
COMPONENTS_WITH_DEMO_PLATFORM = [
'switch', 'light', 'thermostat', 'sensor', 'media_player', 'notify']
'switch', 'light', 'sensor', 'thermostat', 'media_player', 'notify']
def setup(hass, config):

View File

@ -1,52 +1,92 @@
"""
homeassistant.components.tracker
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
homeassistant.components.device_tracker
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Provides functionality to keep track of devices.
device_tracker:
platform: netgear
# Optional
# How many seconds to wait after not seeing device to consider it not home
consider_home: 180
# Seconds between each scan
interval_seconds: 12
# New found devices auto found
track_new_devices: yes
# Maximum distance from home we consider people home
range_home: 100
"""
import logging
import threading
import os
# pylint: disable=too-many-instance-attributes, too-many-arguments
# pylint: disable=too-many-locals
import csv
from datetime import timedelta
import logging
import os
import threading
from homeassistant.helpers import validate_config
from homeassistant.helpers.entity import _OVERWRITE
from homeassistant.bootstrap import prepare_setup_platform
from homeassistant.components import discovery, group
from homeassistant.config import load_yaml_config_file
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_per_platform
from homeassistant.helpers.entity import Entity
import homeassistant.util as util
import homeassistant.util.dt as dt_util
from homeassistant.bootstrap import prepare_setup_platform
from homeassistant.helpers.event import track_utc_time_change
from homeassistant.const import (
STATE_HOME, STATE_NOT_HOME, ATTR_ENTITY_PICTURE, ATTR_FRIENDLY_NAME,
CONF_PLATFORM, DEVICE_DEFAULT_NAME)
from homeassistant.components import group
ATTR_ENTITY_PICTURE, DEVICE_DEFAULT_NAME, STATE_HOME, STATE_NOT_HOME)
DOMAIN = "device_tracker"
DEPENDENCIES = []
SERVICE_DEVICE_TRACKER_RELOAD = "reload_devices_csv"
GROUP_NAME_ALL_DEVICES = 'all devices'
ENTITY_ID_ALL_DEVICES = group.ENTITY_ID_FORMAT.format('all_devices')
ENTITY_ID_FORMAT = DOMAIN + '.{}'
# After how much time do we consider a device not home if
# it does not show up on scans
TIME_DEVICE_NOT_FOUND = timedelta(minutes=3)
CSV_DEVICES = "known_devices.csv"
YAML_DEVICES = 'known_devices.yaml'
# Filename to save known devices to
KNOWN_DEVICES_FILE = "known_devices.csv"
CONF_TRACK_NEW = "track_new_devices"
DEFAULT_CONF_TRACK_NEW = True
CONF_SECONDS = "interval_seconds"
CONF_CONSIDER_HOME = 'consider_home'
DEFAULT_CONSIDER_HOME = 180 # seconds
DEFAULT_CONF_SECONDS = 12
CONF_SCAN_INTERVAL = "interval_seconds"
DEFAULT_SCAN_INTERVAL = 12
TRACK_NEW_DEVICES = "track_new_devices"
CONF_AWAY_HIDE = 'hide_if_away'
DEFAULT_AWAY_HIDE = False
CONF_HOME_RANGE = 'home_range'
DEFAULT_HOME_RANGE = 100
SERVICE_SEE = 'see'
ATTR_LATITUDE = 'latitude'
ATTR_LONGITUDE = 'longitude'
ATTR_MAC = 'mac'
ATTR_DEV_ID = 'dev_id'
ATTR_HOST_NAME = 'host_name'
ATTR_LOCATION_NAME = 'location_name'
ATTR_GPS = 'gps'
ATTR_GPS_ACCURACY = 'gps_accuracy'
ATTR_BATTERY = 'battery'
DISCOVERY_PLATFORMS = {
discovery.SERVICE_NETGEAR: 'netgear',
}
_LOGGER = logging.getLogger(__name__)
# pylint: disable=too-many-arguments
def is_on(hass, entity_id=None):
""" Returns if any or specified device is home. """
@ -55,293 +95,343 @@ def is_on(hass, entity_id=None):
return hass.states.is_state(entity, STATE_HOME)
def see(hass, mac=None, dev_id=None, host_name=None, location_name=None,
gps=None, gps_accuracy=None, battery=None):
""" Call service to notify you see device. """
data = {key: value for key, value in
((ATTR_MAC, mac),
(ATTR_DEV_ID, dev_id),
(ATTR_HOST_NAME, host_name),
(ATTR_LOCATION_NAME, location_name),
(ATTR_GPS, gps)) if value is not None}
hass.services.call(DOMAIN, SERVICE_SEE, data)
def setup(hass, config):
""" Sets up the device tracker. """
""" Setup device tracker """
yaml_path = hass.config.path(YAML_DEVICES)
csv_path = hass.config.path(CSV_DEVICES)
if os.path.isfile(csv_path) and not os.path.isfile(yaml_path) and \
convert_csv_config(csv_path, yaml_path):
os.remove(csv_path)
if not validate_config(config, {DOMAIN: [CONF_PLATFORM]}, _LOGGER):
return False
conf = config.get(DOMAIN, {})
consider_home = timedelta(
seconds=util.convert(conf.get(CONF_CONSIDER_HOME), int,
DEFAULT_CONSIDER_HOME))
track_new = util.convert(conf.get(CONF_TRACK_NEW), bool,
DEFAULT_CONF_TRACK_NEW)
home_range = util.convert(conf.get(CONF_HOME_RANGE), int,
DEFAULT_HOME_RANGE)
tracker_type = config[DOMAIN].get(CONF_PLATFORM)
devices = load_config(yaml_path, hass, consider_home, home_range)
tracker = DeviceTracker(hass, consider_home, track_new, home_range,
devices)
tracker_implementation = \
prepare_setup_platform(hass, config, DOMAIN, tracker_type)
if tracker_implementation is None:
_LOGGER.error("Unknown device_tracker type specified: %s.",
tracker_type)
return False
device_scanner = tracker_implementation.get_scanner(hass, config)
if device_scanner is None:
_LOGGER.error("Failed to initialize device scanner: %s",
tracker_type)
return False
seconds = util.convert(config[DOMAIN].get(CONF_SECONDS), int,
DEFAULT_CONF_SECONDS)
track_new_devices = config[DOMAIN].get(TRACK_NEW_DEVICES) or False
_LOGGER.info("Tracking new devices: %s", track_new_devices)
tracker = DeviceTracker(hass, device_scanner, seconds, track_new_devices)
# We only succeeded if we got to parse the known devices file
return not tracker.invalid_known_devices_file
class DeviceTracker(object):
""" Class that tracks which devices are home and which are not. """
def __init__(self, hass, device_scanner, seconds, track_new_devices):
self.hass = hass
self.device_scanner = device_scanner
self.lock = threading.Lock()
# Do we track new devices by default?
self.track_new_devices = track_new_devices
# Dictionary to keep track of known devices and devices we track
self.tracked = {}
self.untracked_devices = set()
# Did we encounter an invalid known devices file
self.invalid_known_devices_file = False
# Wrap it in a func instead of lambda so it can be identified in
# the bus by its __name__ attribute.
def update_device_state(now):
""" Triggers update of the device states. """
self.update_devices(now)
dev_group = group.Group(
hass, GROUP_NAME_ALL_DEVICES, user_defined=False)
def reload_known_devices_service(service):
""" Reload known devices file. """
self._read_known_devices_file()
self.update_devices(dt_util.utcnow())
dev_group.update_tracked_entity_ids(self.device_entity_ids)
reload_known_devices_service(None)
if self.invalid_known_devices_file:
return
seconds = range(0, 60, seconds)
_LOGGER.info("Device tracker interval second=%s", seconds)
track_utc_time_change(hass, update_device_state, second=seconds)
hass.services.register(DOMAIN,
SERVICE_DEVICE_TRACKER_RELOAD,
reload_known_devices_service)
@property
def device_entity_ids(self):
""" Returns a set containing all device entity ids
that are being tracked. """
return set(device['entity_id'] for device in self.tracked.values())
def _update_state(self, now, device, is_home):
""" Update the state of a device. """
dev_info = self.tracked[device]
if is_home:
# Update last seen if at home
dev_info['last_seen'] = now
else:
# State remains at home if it has been seen in the last
# TIME_DEVICE_NOT_FOUND
is_home = now - dev_info['last_seen'] < TIME_DEVICE_NOT_FOUND
state = STATE_HOME if is_home else STATE_NOT_HOME
# overwrite properties that have been set in the config file
attr = dict(dev_info['state_attr'])
attr.update(_OVERWRITE.get(dev_info['entity_id'], {}))
self.hass.states.set(
dev_info['entity_id'], state, attr)
def update_devices(self, now):
""" Update device states based on the found devices. """
if not self.lock.acquire(False):
def setup_platform(p_type, p_config, disc_info=None):
""" Setup a device tracker platform. """
platform = prepare_setup_platform(hass, config, DOMAIN, p_type)
if platform is None:
return
try:
found_devices = set(dev.upper() for dev in
self.device_scanner.scan_devices())
if hasattr(platform, 'get_scanner'):
scanner = platform.get_scanner(hass, {DOMAIN: p_config})
for device in self.tracked:
is_home = device in found_devices
if scanner is None:
_LOGGER.error('Error setting up platform %s', p_type)
return
self._update_state(now, device, is_home)
setup_scanner_platform(hass, p_config, scanner, tracker.see)
return
if is_home:
found_devices.remove(device)
if not platform.setup_scanner(hass, p_config, tracker.see):
_LOGGER.error('Error setting up platform %s', p_type)
except Exception: # pylint: disable=broad-except
_LOGGER.exception('Error setting up platform %s', p_type)
# Did we find any devices that we didn't know about yet?
new_devices = found_devices - self.untracked_devices
for p_type, p_config in \
config_per_platform(config, DOMAIN, _LOGGER):
setup_platform(p_type, p_config)
if new_devices:
if not self.track_new_devices:
self.untracked_devices.update(new_devices)
def device_tracker_discovered(service, info):
""" Called when a device tracker platform is discovered. """
setup_platform(DISCOVERY_PLATFORMS[service], {}, info)
self._update_known_devices_file(new_devices)
finally:
self.lock.release()
discovery.listen(hass, DISCOVERY_PLATFORMS.keys(),
device_tracker_discovered)
# pylint: disable=too-many-branches
def _read_known_devices_file(self):
""" Parse and process the known devices file. """
known_dev_path = self.hass.config.path(KNOWN_DEVICES_FILE)
def update_stale(now):
""" Clean up stale devices. """
tracker.update_stale(now)
track_utc_time_change(hass, update_stale, second=range(0, 60, 5))
# Return if no known devices file exists
if not os.path.isfile(known_dev_path):
tracker.setup_group()
def see_service(call):
""" Service to see a device. """
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_GPS, ATTR_GPS_ACCURACY, ATTR_BATTERY)}
tracker.see(**args)
hass.services.register(DOMAIN, SERVICE_SEE, see_service)
return True
class DeviceTracker(object):
""" Track devices """
def __init__(self, hass, consider_home, track_new, home_range, devices):
self.hass = hass
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.consider_home = consider_home
self.track_new = track_new
self.home_range = home_range
self.lock = threading.Lock()
for device in devices:
if device.track:
device.update_ha_state()
self.group = None
def see(self, mac=None, dev_id=None, host_name=None, location_name=None,
gps=None, gps_accuracy=None, battery=None):
""" Notify device tracker that you see a device. """
with self.lock:
if mac is None and dev_id is None:
raise HomeAssistantError('Neither mac or device id passed in')
elif mac is not None:
mac = mac.upper()
device = self.mac_to_dev.get(mac)
if not device:
dev_id = util.slugify(host_name or '') or util.slugify(mac)
else:
dev_id = str(dev_id).lower()
device = self.devices.get(dev_id)
if device:
device.seen(host_name, location_name, gps, gps_accuracy,
battery)
if device.track:
device.update_ha_state()
return
# If no device can be found, create it
device = Device(
self.hass, self.consider_home, self.home_range, self.track_new,
dev_id, mac, (host_name or dev_id).replace('_', ' '))
self.devices[dev_id] = device
if mac is not None:
self.mac_to_dev[mac] = device
device.seen(host_name, location_name, gps, gps_accuracy, battery)
if device.track:
device.update_ha_state()
# During init, we ignore the group
if self.group is not None:
self.group.update_tracked_entity_ids(
list(self.group.tracking) + [device.entity_id])
update_config(self.hass.config.path(YAML_DEVICES), dev_id, device)
def setup_group(self):
""" Initializes group for all tracked devices. """
entity_ids = (dev.entity_id for dev in self.devices.values()
if dev.track)
self.group = group.setup_group(
self.hass, GROUP_NAME_ALL_DEVICES, entity_ids, False)
def update_stale(self, now):
""" Update stale devices. """
with self.lock:
for device in self.devices.values():
if (device.track and device.last_update_home and
device.stale(now)):
device.update_ha_state(True)
class Device(Entity):
""" Tracked device. """
host_name = None
location_name = None
gps = None
gps_accuracy = 0
last_seen = None
battery = None
# Track if the last update of this device was HOME
last_update_home = False
_state = STATE_NOT_HOME
def __init__(self, hass, consider_home, home_range, track, dev_id, mac,
name=None, picture=None, away_hide=False):
self.hass = hass
self.entity_id = ENTITY_ID_FORMAT.format(dev_id)
# Timedelta object how long we consider a device home if it is not
# detected anymore.
self.consider_home = consider_home
# Distance in meters
self.home_range = home_range
# Device ID
self.dev_id = dev_id
self.mac = mac
# If we should track this device
self.track = track
# Configured name
self.config_name = name
# Configured picture
self.config_picture = picture
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
def name(self):
""" Returns the name of the entity. """
return self.config_name or self.host_name or DEVICE_DEFAULT_NAME
@property
def state(self):
""" State of the device. """
return self._state
@property
def state_attributes(self):
""" Device state attributes. """
attr = {}
if self.config_picture:
attr[ATTR_ENTITY_PICTURE] = self.config_picture
if self.gps:
attr[ATTR_LATITUDE] = self.gps[0]
attr[ATTR_LONGITUDE] = self.gps[1]
attr[ATTR_GPS_ACCURACY] = self.gps_accuracy
if self.battery:
attr[ATTR_BATTERY] = self.battery
return attr
@property
def hidden(self):
""" If device should be hidden. """
return self.away_hide and self.state != STATE_HOME
def seen(self, host_name=None, location_name=None, gps=None,
gps_accuracy=0, battery=None):
""" Mark the device as seen. """
self.last_seen = dt_util.utcnow()
self.host_name = host_name
self.location_name = location_name
self.gps_accuracy = gps_accuracy
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()
def stale(self, now=None):
""" Return if device state is stale. """
return self.last_seen and \
(now or dt_util.utcnow()) - self.last_seen > self.consider_home
def update(self):
""" Update state of entity. """
if not self.last_seen:
return
elif self.location_name:
self._state = self.location_name
elif self.gps is not None:
self._state = STATE_HOME if self.gps_home else STATE_NOT_HOME
elif self.stale():
self._state = STATE_NOT_HOME
self.last_update_home = False
else:
self._state = STATE_HOME
self.last_update_home = True
self.lock.acquire()
self.untracked_devices.clear()
def convert_csv_config(csv_path, yaml_path):
""" Convert CSV config file format to YAML. """
used_ids = set()
with open(csv_path) as inp:
for row in csv.DictReader(inp):
dev_id = util.ensure_unique_string(
(util.slugify(row['name']) or DEVICE_DEFAULT_NAME).lower(),
used_ids)
used_ids.add(dev_id)
device = Device(None, None, None, row['track'] == '1', dev_id,
row['device'], row['name'], row['picture'])
update_config(yaml_path, dev_id, device)
return True
with open(known_dev_path) as inp:
# To track which devices need an entity_id assigned
need_entity_id = []
def load_config(path, hass, consider_home, home_range):
""" Load devices from YAML config file. """
if not os.path.isfile(path):
return []
return [
Device(hass, consider_home, home_range, device.get('track', False),
str(dev_id).lower(), str(device.get('mac')).upper(),
device.get('name'), device.get('picture'),
device.get(CONF_AWAY_HIDE, DEFAULT_AWAY_HIDE))
for dev_id, device in load_yaml_config_file(path).items()]
# All devices that are still in this set after we read the CSV file
# have been removed from the file and thus need to be cleaned up.
removed_devices = set(self.tracked.keys())
try:
for row in csv.DictReader(inp):
device = row['device'].upper()
def setup_scanner_platform(hass, config, scanner, see_device):
""" Helper method to connect scanner-based platform to device tracker. """
interval = util.convert(config.get(CONF_SCAN_INTERVAL), int,
DEFAULT_SCAN_INTERVAL)
if row['track'] == '1':
if device in self.tracked:
# Device exists
removed_devices.remove(device)
else:
# We found a new device
need_entity_id.append(device)
# Initial scan of each mac we also tell about host name for config
seen = set()
self._track_device(device, row['name'])
def device_tracker_scan(now):
""" Called when interval matches. """
for mac in scanner.scan_devices():
if mac in seen:
host_name = None
else:
host_name = scanner.get_device_name(mac)
seen.add(mac)
see_device(mac=mac, host_name=host_name)
# Update state_attr with latest from file
state_attr = {
ATTR_FRIENDLY_NAME: row['name']
}
track_utc_time_change(hass, device_tracker_scan, second=range(0, 60,
interval))
if row['picture']:
state_attr[ATTR_ENTITY_PICTURE] = row['picture']
device_tracker_scan(None)
self.tracked[device]['state_attr'] = state_attr
else:
self.untracked_devices.add(device)
def update_config(path, dev_id, device):
""" Add device to YAML config file. """
with open(path, 'a') as out:
out.write('\n')
out.write('{}:\n'.format(device.dev_id))
# Remove existing devices that we no longer track
for device in removed_devices:
entity_id = self.tracked[device]['entity_id']
_LOGGER.info("Removing entity %s", entity_id)
self.hass.states.remove(entity_id)
self.tracked.pop(device)
self._generate_entity_ids(need_entity_id)
if not self.tracked:
_LOGGER.warning(
"No devices to track. Please update %s.",
known_dev_path)
_LOGGER.info("Loaded devices from %s", known_dev_path)
except KeyError:
self.invalid_known_devices_file = True
_LOGGER.warning(
("Invalid known devices file: %s. "
"We won't update it with new found devices."),
known_dev_path)
finally:
self.lock.release()
def _update_known_devices_file(self, new_devices):
""" Add new devices to known devices file. """
if not self.invalid_known_devices_file:
known_dev_path = self.hass.config.path(KNOWN_DEVICES_FILE)
try:
# If file does not exist we will write the header too
is_new_file = not os.path.isfile(known_dev_path)
with open(known_dev_path, 'a') as outp:
_LOGGER.info("Found %d new devices, updating %s",
len(new_devices), known_dev_path)
writer = csv.writer(outp)
if is_new_file:
writer.writerow(("device", "name", "track", "picture"))
for device in new_devices:
# See if the device scanner knows the name
# else defaults to unknown device
name = self.device_scanner.get_device_name(device) or \
DEVICE_DEFAULT_NAME
track = 0
if self.track_new_devices:
self._track_device(device, name)
track = 1
writer.writerow((device, name, track, ""))
if self.track_new_devices:
self._generate_entity_ids(new_devices)
except IOError:
_LOGGER.exception("Error updating %s with %d new devices",
known_dev_path, len(new_devices))
def _track_device(self, device, name):
"""
Add a device to the list of tracked devices.
Does not generate the entity id yet.
"""
default_last_seen = dt_util.utcnow().replace(year=1990)
self.tracked[device] = {
'name': name,
'last_seen': default_last_seen,
'state_attr': {ATTR_FRIENDLY_NAME: name}
}
def _generate_entity_ids(self, need_entity_id):
""" Generate entity ids for a list of devices. """
# Setup entity_ids for the new devices
used_entity_ids = [info['entity_id'] for device, info
in self.tracked.items()
if device not in need_entity_id]
for device in need_entity_id:
name = self.tracked[device]['name']
entity_id = util.ensure_unique_string(
ENTITY_ID_FORMAT.format(util.slugify(name)),
used_entity_ids)
used_entity_ids.append(entity_id)
self.tracked[device]['entity_id'] = entity_id
for key, value in (('name', device.name), ('mac', device.mac),
('picture', device.config_picture),
('track', 'yes' if device.track else 'no'),
(CONF_AWAY_HIDE,
'yes' if device.away_hide else 'no')):
out.write(' {}: {}\n'.format(key, '' if value is None else value))

View File

@ -157,11 +157,19 @@ class AsusWrtDeviceScanner(object):
devices = {}
for lease in leases_result:
match = _LEASES_REGEX.search(lease.decode('utf-8'))
# For leases where the client doesn't set a hostname, ensure
# it is blank and not '*', which breaks the entity_id down
# the line
host = match.group('host')
if host == '*':
host = ''
devices[match.group('ip')] = {
'host': host,
'status': '',
'ip': match.group('ip'),
'mac': match.group('mac').upper(),
'host': match.group('host'),
'status': ''
}
for neighbor in neighbors:

View 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

View File

@ -0,0 +1,48 @@
"""
homeassistant.components.device_tracker.mqtt
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
MQTT platform for the device tracker.
device_tracker:
platform: mqtt
qos: 1
devices:
paulus_oneplus: /location/paulus
annetherese_n4: /location/annetherese
"""
import logging
from homeassistant import util
import homeassistant.components.mqtt as mqtt
DEPENDENCIES = ['mqtt']
CONF_QOS = 'qos'
CONF_DEVICES = 'devices'
DEFAULT_QOS = 0
_LOGGER = logging.getLogger(__name__)
def setup_scanner(hass, config, see):
""" Set up a MQTT tracker. """
devices = config.get(CONF_DEVICES)
qos = util.convert(config.get(CONF_QOS), int, DEFAULT_QOS)
if not isinstance(devices, dict):
_LOGGER.error('Expected %s to be a dict, found %s', CONF_DEVICES,
devices)
return False
dev_id_lookup = {}
def device_tracker_message_received(topic, payload, qos):
""" MQTT message received. """
see(dev_id=dev_id_lookup[topic], location_name=payload)
for dev_id, topic in devices.items():
dev_id_lookup[topic] = dev_id
mqtt.subscribe(hass, topic, device_tracker_message_received, qos)
return True

View File

@ -70,7 +70,6 @@ class NetgearDeviceScanner(object):
self.lock = threading.Lock()
if host is None:
print("BIER")
self._api = pynetgear.Netgear()
elif username is None:
self._api = pynetgear.Netgear(password, host)

View File

@ -44,7 +44,7 @@ _LOGGER = logging.getLogger(__name__)
# interval in minutes to exclude devices from a scan while they are home
CONF_HOME_INTERVAL = "home_interval"
REQUIREMENTS = ['python-nmap==0.4.1']
REQUIREMENTS = ['python-nmap==0.4.3']
def get_scanner(hass, config):

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

View File

@ -19,22 +19,22 @@ from homeassistant.const import (
DOMAIN = "discovery"
DEPENDENCIES = []
REQUIREMENTS = ['netdisco==0.3']
REQUIREMENTS = ['netdisco==0.4.1']
SCAN_INTERVAL = 300 # seconds
# Next 3 lines for now a mirror from netdisco.const
# Should setup a mapping netdisco.const -> own constants
SERVICE_WEMO = 'belkin_wemo'
SERVICE_HUE = 'philips_hue'
SERVICE_CAST = 'google_cast'
SERVICE_NETGEAR = 'netgear_router'
SERVICE_SONOS = 'sonos'
SERVICE_HANDLERS = {
SERVICE_WEMO: "switch",
SERVICE_CAST: "media_player",
SERVICE_HUE: "light",
SERVICE_NETGEAR: 'device_tracker',
SERVICE_SONOS: 'media_player',
}
@ -79,13 +79,6 @@ def setup(hass, config):
if not component:
return
# Hack - fix when device_tracker supports discovery
if service == SERVICE_NETGEAR:
bootstrap.setup_component(hass, component, {
'device_tracker': {'platform': 'netgear'}
})
return
# This component cannot be setup.
if not bootstrap.setup_component(hass, component, config):
return

View File

@ -21,7 +21,8 @@ _LOGGER = logging.getLogger(__name__)
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/]+)|)')

View File

@ -1,2 +1,2 @@
""" DO NOT MODIFY. Auto-generated by build_frontend script """
VERSION = "35ecb5457a9ff0f4142c2605b53eb843"
VERSION = "3a3ed81f9d66bf24e17f1d02b8403335"

File diff suppressed because one or more lines are too long

@ -1 +1 @@
Subproject commit b0b12e20e0f61df849c414c2dfbcf9923f784631
Subproject commit 6989009b2d59e39fd39b3025ff5899877f618bd3

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

View File

@ -147,8 +147,6 @@ def _api_history_period(handler, path_match, data):
end_time = start_time + one_day
print("Fetchign", start_time, end_time)
entity_id = data.get('filter_entity_id')
handler.write_json(

View File

@ -205,7 +205,7 @@ class HomeAssistantHTTPServer(ThreadingMixIn, HTTPServer):
self.serve_forever()
def register_path(self, method, url, callback, require_auth=True):
""" Registers a path wit the server. """
""" Registers a path with the server. """
self.paths.append((method, url, callback, require_auth))
def log_message(self, fmt, *args):
@ -487,7 +487,7 @@ class ServerSession:
return self._expiry < date_util.utcnow()
class SessionStore:
class SessionStore(object):
""" Responsible for storing and retrieving http sessions """
def __init__(self, enabled=True):
""" Set up the session store """

View File

@ -8,10 +8,11 @@ import logging
from homeassistant.components.light import Light, ATTR_BRIGHTNESS
from homeassistant.const import ATTR_FRIENDLY_NAME
import tellcore.constants as tellcore_constants
from tellcore.library import DirectCallbackDispatcher
REQUIREMENTS = ['tellcore-py==1.0.4']
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
""" Find and return Tellstick lights. """
@ -22,13 +23,28 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
"Failed to import tellcore")
return []
core = telldus.TelldusCore()
# pylint: disable=no-member
if telldus.TelldusCore.callback_dispatcher is None:
dispatcher = DirectCallbackDispatcher()
core = telldus.TelldusCore(callback_dispatcher=dispatcher)
else:
core = telldus.TelldusCore()
switches_and_lights = core.devices()
lights = []
for switch in switches_and_lights:
if switch.methods(tellcore_constants.TELLSTICK_DIM):
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_:
light_device.update_ha_state(True)
core.register_device_event(_device_event_callback)
add_devices_callback(lights)
@ -40,15 +56,15 @@ class TellstickLight(Light):
tellcore_constants.TELLSTICK_UP |
tellcore_constants.TELLSTICK_DOWN)
def __init__(self, tellstick):
self.tellstick = tellstick
self.state_attr = {ATTR_FRIENDLY_NAME: tellstick.name}
def __init__(self, tellstick_device):
self.tellstick_device = tellstick_device
self.state_attr = {ATTR_FRIENDLY_NAME: tellstick_device.name}
self._brightness = 0
@property
def name(self):
""" Returns the name of the switch if any. """
return self.tellstick.name
return self.tellstick_device.name
@property
def is_on(self):
@ -62,8 +78,9 @@ class TellstickLight(Light):
def turn_off(self, **kwargs):
""" Turns the switch off. """
self.tellstick.turn_off()
self.tellstick_device.turn_off()
self._brightness = 0
self.update_ha_state()
def turn_on(self, **kwargs):
""" Turns the switch on. """
@ -74,11 +91,12 @@ class TellstickLight(Light):
else:
self._brightness = brightness
self.tellstick.dim(self._brightness)
self.tellstick_device.dim(self._brightness)
self.update_ha_state()
def update(self):
""" 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)
if last_command == tellcore_constants.TELLSTICK_TURNON:
@ -88,6 +106,11 @@ class TellstickLight(Light):
elif (last_command == tellcore_constants.TELLSTICK_DIM or
last_command == tellcore_constants.TELLSTICK_UP or
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:
self._brightness = last_sent_value
@property
def should_poll(self):
""" Tells Home Assistant not to poll this entity. """
return False

View File

@ -12,9 +12,10 @@ from homeassistant.core import State, DOMAIN as HA_DOMAIN
from homeassistant.const import (
EVENT_STATE_CHANGED, STATE_HOME, STATE_ON, STATE_OFF,
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, HTTP_BAD_REQUEST)
from homeassistant import util
import homeassistant.util.dt as dt_util
import homeassistant.components.recorder as recorder
import homeassistant.components.sun as sun
from homeassistant.components import recorder, sun
DOMAIN = "logbook"
DEPENDENCIES = ['recorder', 'http']
@ -25,8 +26,29 @@ QUERY_EVENTS_BETWEEN = """
SELECT * FROM events WHERE time_fired > ? AND time_fired < ?
"""
EVENT_LOGBOOK_ENTRY = 'LOGBOOK_ENTRY'
GROUP_BY_MINUTES = 15
ATTR_NAME = 'name'
ATTR_MESSAGE = 'message'
ATTR_DOMAIN = 'domain'
ATTR_ENTITY_ID = 'entity_id'
def log_entry(hass, name, message, domain=None, entity_id=None):
""" Adds an entry to the logbook. """
data = {
ATTR_NAME: name,
ATTR_MESSAGE: message
}
if domain is not None:
data[ATTR_DOMAIN] = domain
if entity_id is not None:
data[ATTR_ENTITY_ID] = entity_id
hass.bus.fire(EVENT_LOGBOOK_ENTRY, data)
def setup(hass, config):
""" Listens for download events to download files. """
@ -110,7 +132,10 @@ def humanify(events):
# Process events
for event in events_batch:
if event.event_type == EVENT_STATE_CHANGED:
entity_id = event.data['entity_id']
entity_id = event.data.get('entity_id')
if entity_id is None:
continue
if entity_id.startswith('sensor.'):
last_sensor_event[entity_id] = event
@ -175,6 +200,20 @@ def humanify(events):
event.time_fired, "Home Assistant", action,
domain=HA_DOMAIN)
elif event.event_type == EVENT_LOGBOOK_ENTRY:
domain = event.data.get(ATTR_DOMAIN)
entity_id = event.data.get(ATTR_ENTITY_ID)
if domain is None and entity_id is not None:
try:
domain = util.split_entity_id(str(entity_id))[0]
except IndexError:
pass
yield Entry(
event.time_fired, event.data.get(ATTR_NAME),
event.data.get(ATTR_MESSAGE), domain,
entity_id)
def _entry_message_from_state(domain, state):
""" Convert a state to a message for the logbook. """

View File

@ -19,12 +19,13 @@ from homeassistant.const import (
DOMAIN = 'media_player'
DEPENDENCIES = []
SCAN_INTERVAL = 30
SCAN_INTERVAL = 10
ENTITY_ID_FORMAT = DOMAIN + '.{}'
DISCOVERY_PLATFORMS = {
discovery.SERVICE_CAST: 'cast',
discovery.SERVICE_SONOS: 'sonos',
}
SERVICE_YOUTUBE_VIDEO = 'play_youtube_video'
@ -483,7 +484,7 @@ class MediaPlayerDevice(Entity):
else:
state_attr = {
attr: getattr(self, attr) for attr
in ATTR_TO_PROPERTY if getattr(self, attr)
in ATTR_TO_PROPERTY if getattr(self, attr) is not None
}
if self.media_image_url:

View File

@ -0,0 +1,445 @@
"""
homeassistant.components.media_player.itunes
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Provides an interface to iTunes-API (https://github.com/maddox/itunes-api)
The iTunes media player will allow you to control your iTunes instance. You
can play/pause/next/previous/mute, adjust volume, etc.
In addition to controlling iTunes, your available AirPlay endpoints will be
added as media players as well. You can then individually address them append
turn them on, turn them off, or adjust their volume.
Configuration:
To use iTunes you will need to add something like the following to
your configuration.yaml file.
media_player:
platform: itunes
name: iTunes
host: http://192.168.1.16
port: 8181
Variables:
name
*Optional
The name of the device.
url
*Required
URL of your running version of iTunes-API. Example: http://192.168.1.50:8181
"""
import logging
from homeassistant.components.media_player import (
MediaPlayerDevice, MEDIA_TYPE_MUSIC, SUPPORT_PAUSE, SUPPORT_SEEK,
SUPPORT_VOLUME_SET, SUPPORT_VOLUME_MUTE, SUPPORT_PREVIOUS_TRACK,
SUPPORT_NEXT_TRACK, SUPPORT_TURN_ON, SUPPORT_TURN_OFF,
ATTR_ENTITY_PICTURE, ATTR_SUPPORTED_MEDIA_COMMANDS)
from homeassistant.const import (
STATE_IDLE, STATE_PLAYING, STATE_PAUSED, STATE_OFF, STATE_ON)
import requests
_LOGGER = logging.getLogger(__name__)
SUPPORT_ITUNES = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_SEEK
SUPPORT_AIRPLAY = SUPPORT_VOLUME_SET | SUPPORT_TURN_ON | SUPPORT_TURN_OFF
DOMAIN = 'itunes'
class Itunes(object):
""" itunes-api client. """
def __init__(self, host, port):
self.host = host
self.port = port
@property
def _base_url(self):
""" Returns the base url for endpoints. """
return self.host + ":" + str(self.port)
def _request(self, method, path, params=None):
""" Makes the actual request and returns the parsed response. """
url = self._base_url + path
try:
if method == 'GET':
response = requests.get(url)
elif method == "POST":
response = requests.put(url, params)
elif method == "PUT":
response = requests.put(url, params)
elif method == "DELETE":
response = requests.delete(url)
return response.json()
except requests.exceptions.HTTPError:
return {'player_state': 'error'}
except requests.exceptions.RequestException:
return {'player_state': 'offline'}
def _command(self, named_command):
""" Makes a request for a controlling command. """
return self._request('PUT', '/' + named_command)
def now_playing(self):
""" Returns the current state. """
return self._request('GET', '/now_playing')
def set_volume(self, level):
""" Sets the volume and returns the current state, level 0-100. """
return self._request('PUT', '/volume', {'level': level})
def set_muted(self, muted):
""" Mutes and returns the current state, muted True or False. """
return self._request('PUT', '/mute', {'muted': muted})
def play(self):
""" Sets playback to play and returns the current state. """
return self._command('play')
def pause(self):
""" Sets playback to paused and returns the current state. """
return self._command('pause')
def next(self):
""" Skips to the next track and returns the current state. """
return self._command('next')
def previous(self):
""" Skips back and returns the current state. """
return self._command('previous')
def artwork_url(self):
""" Returns a URL of the current track's album art. """
return self._base_url + '/artwork'
def airplay_devices(self):
""" Returns a list of AirPlay devices. """
return self._request('GET', '/airplay_devices')
def airplay_device(self, device_id):
""" Returns an AirPlay device. """
return self._request('GET', '/airplay_devices/' + device_id)
def toggle_airplay_device(self, device_id, toggle):
""" Toggles airplay device on or off, id, toggle True or False. """
command = 'on' if toggle else 'off'
path = '/airplay_devices/' + device_id + '/' + command
return self._request('PUT', path)
def set_volume_airplay_device(self, device_id, level):
""" Sets volume, returns current state of device, id,level 0-100. """
path = '/airplay_devices/' + device_id + '/volume'
return self._request('PUT', path, {'level': level})
# pylint: disable=unused-argument
# pylint: disable=abstract-method
# pylint: disable=too-many-instance-attributes
def setup_platform(hass, config, add_devices, discovery_info=None):
""" Sets up the itunes platform. """
add_devices([
ItunesDevice(
config.get('name', 'iTunes'),
config.get('host'),
config.get('port'),
add_devices
)
])
class ItunesDevice(MediaPlayerDevice):
""" Represents a iTunes-API instance. """
# pylint: disable=too-many-public-methods
def __init__(self, name, host, port, add_devices):
self._name = name
self._host = host
self._port = port
self._add_devices = add_devices
self.client = Itunes(self._host, self._port)
self.current_volume = None
self.muted = None
self.current_title = None
self.current_album = None
self.current_artist = None
self.current_playlist = None
self.content_id = None
self.player_state = None
self.airplay_devices = {}
self.update()
def update_state(self, state_hash):
""" Update all the state properties with the passed in dictionary. """
self.player_state = state_hash.get('player_state', None)
self.current_volume = state_hash.get('volume', 0)
self.muted = state_hash.get('muted', None)
self.current_title = state_hash.get('name', None)
self.current_album = state_hash.get('album', None)
self.current_artist = state_hash.get('artist', None)
self.current_playlist = state_hash.get('playlist', None)
self.content_id = state_hash.get('id', None)
@property
def name(self):
""" Returns the name of the device. """
return self._name
@property
def state(self):
""" Returns the state of the device. """
if self.player_state == 'offline' or self.player_state is None:
return 'offline'
if self.player_state == 'error':
return 'error'
if self.player_state == 'stopped':
return STATE_IDLE
if self.player_state == 'paused':
return STATE_PAUSED
else:
return STATE_PLAYING
def update(self):
""" Retrieve latest state. """
now_playing = self.client.now_playing()
self.update_state(now_playing)
found_devices = self.client.airplay_devices()
found_devices = found_devices.get('airplay_devices', [])
new_devices = []
for device_data in found_devices:
device_id = device_data.get('id')
if self.airplay_devices.get(device_id):
# update it
airplay_device = self.airplay_devices.get(device_id)
airplay_device.update_state(device_data)
else:
# add it
airplay_device = AirPlayDevice(device_id, self.client)
airplay_device.update_state(device_data)
self.airplay_devices[device_id] = airplay_device
new_devices.append(airplay_device)
if new_devices:
self._add_devices(new_devices)
@property
def is_volume_muted(self):
""" Boolean if volume is currently muted. """
return self.muted
@property
def volume_level(self):
""" Volume level of the media player (0..1). """
return self.current_volume/100.0
@property
def media_content_id(self):
""" Content ID of current playing media. """
return self.content_id
@property
def media_content_type(self):
""" Content type of current playing media. """
return MEDIA_TYPE_MUSIC
@property
def media_image_url(self):
""" Image url of current playing media. """
if self.player_state in (STATE_PLAYING, STATE_IDLE, STATE_PAUSED) and \
self.current_title is not None:
return self.client.artwork_url()
else:
return 'https://cloud.githubusercontent.com/assets/260/9829355' \
'/33fab972-58cf-11e5-8ea2-2ca74bdaae40.png'
@property
def media_title(self):
""" Title of current playing media. """
return self.current_title
@property
def media_artist(self):
""" Artist of current playing media. (Music track only) """
return self.current_artist
@property
def media_album_name(self):
""" Album of current playing media. (Music track only) """
return self.current_album
@property
def supported_media_commands(self):
""" Flags of media commands that are supported. """
return SUPPORT_ITUNES
def set_volume_level(self, volume):
""" set volume level, range 0..1. """
response = self.client.set_volume(int(volume * 100))
self.update_state(response)
def mute_volume(self, mute):
""" mute (true) or unmute (false) media player. """
response = self.client.set_muted(mute)
self.update_state(response)
def media_play(self):
""" media_play media player. """
response = self.client.play()
self.update_state(response)
def media_pause(self):
""" media_pause media player. """
response = self.client.pause()
self.update_state(response)
def media_next_track(self):
""" media_next media player. """
response = self.client.next()
self.update_state(response)
def media_previous_track(self):
""" media_previous media player. """
response = self.client.previous()
self.update_state(response)
class AirPlayDevice(MediaPlayerDevice):
""" Represents an AirPlay device via an iTunes-API instance. """
# pylint: disable=too-many-public-methods
def __init__(self, device_id, client):
self._id = device_id
self.client = client
self.device_name = "AirPlay"
self.kind = None
self.active = False
self.selected = False
self.volume = 0
self.supports_audio = False
self.supports_video = False
self.player_state = None
def update_state(self, state_hash):
""" Update all the state properties with the passed in dictionary. """
if 'player_state' in state_hash:
self.player_state = state_hash.get('player_state', None)
if 'name' in state_hash:
name = state_hash.get('name', '')
self.device_name = (name + ' AirTunes Speaker').strip()
if 'kind' in state_hash:
self.kind = state_hash.get('kind', None)
if 'active' in state_hash:
self.active = state_hash.get('active', None)
if 'selected' in state_hash:
self.selected = state_hash.get('selected', None)
if 'sound_volume' in state_hash:
self.volume = state_hash.get('sound_volume', 0)
if 'supports_audio' in state_hash:
self.supports_audio = state_hash.get('supports_audio', None)
if 'supports_video' in state_hash:
self.supports_video = state_hash.get('supports_video', None)
@property
def name(self):
""" Returns the name of the device. """
return self.device_name
@property
def state(self):
""" Returns the state of the device. """
if self.selected is True:
return STATE_ON
else:
return STATE_OFF
def update(self):
""" Retrieve latest state. """
@property
def volume_level(self):
return float(self.volume)/100.0
@property
def media_content_type(self):
return MEDIA_TYPE_MUSIC
@property
def supported_media_commands(self):
""" Flags of media commands that are supported. """
return SUPPORT_AIRPLAY
@property
def device_state_attributes(self):
""" Return the state attributes. """
state_attr = {}
state_attr[ATTR_SUPPORTED_MEDIA_COMMANDS] = SUPPORT_AIRPLAY
if self.state == STATE_OFF:
state_attr[ATTR_ENTITY_PICTURE] = \
('https://cloud.githubusercontent.com/assets/260/9833073'
'/6eb5c906-5958-11e5-9b4a-472cdf36be16.png')
else:
state_attr[ATTR_ENTITY_PICTURE] = \
('https://cloud.githubusercontent.com/assets/260/9833072'
'/6eb13cce-5958-11e5-996f-e2aaefbc9a24.png')
return state_attr
def set_volume_level(self, volume):
""" set volume level, range 0..1. """
volume = int(volume * 100)
response = self.client.set_volume_airplay_device(self._id, volume)
self.update_state(response)
def turn_on(self):
""" Select AirPlay. """
self.update_state({"selected": True})
self.update_ha_state()
response = self.client.toggle_airplay_device(self._id, True)
self.update_state(response)
def turn_off(self):
""" Deselect AirPlay. """
self.update_state({"selected": False})
self.update_ha_state()
response = self.client.toggle_airplay_device(self._id, False)
self.update_state(response)

View File

@ -107,6 +107,7 @@ class KodiDevice(MediaPlayerDevice):
try:
return self._server.Player.GetActivePlayers()
except jsonrpc_requests.jsonrpc.TransportError:
_LOGGER.exception('Unable to fetch kodi data')
return None
@property

View File

@ -0,0 +1,188 @@
"""
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 homeassistant.components.media_player import (
MediaPlayerDevice, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK,
SUPPORT_NEXT_TRACK, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO)
from homeassistant.const import (
STATE_IDLE, STATE_PLAYING, STATE_PAUSED, STATE_UNKNOWN)
REQUIREMENTS = ['https://github.com/miniconfig/python-plexapi/archive/'
'437e36dca3b7780dc0cb73941d662302c0cd2fa9.zip'
'#python-plexapi==1.0.2']
_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
name = config.get('name', '')
user = config.get('user', '')
password = config.get('password', '')
plexuser = MyPlexUser.signin(user, password)
plexserver = plexuser.getResource(name).connect()
dev = plexserver.clients()
for device in dev:
if "PlayStation" not in device.name:
add_devices([PlexClient(device.name, plexserver)])
class PlexClient(MediaPlayerDevice):
""" Represents a Plex device. """
# pylint: disable=too-many-public-methods
def __init__(self, name, plexserver):
self.client = plexserver.client(name)
self._name = name
self._media = None
self.update()
self.server = plexserver
@property
def name(self):
""" Returns the name of the device. """
return self._name
@property
def state(self):
""" Returns the state of the device. """
if self._media is None:
return STATE_IDLE
else:
state = self._media.get('state')
if state == 'playing':
return STATE_PLAYING
elif state == 'paused':
return STATE_PAUSED
return STATE_UNKNOWN
def update(self):
timeline = self.client.timeline()
for timeline_item in timeline:
if timeline_item.get('state') in ('playing', 'paused'):
self._media = timeline_item
@property
def media_content_id(self):
""" Content ID of current playing media. """
if self._media is not None:
return self._media.get('ratingKey')
@property
def media_content_type(self):
""" Content type of current playing media. """
if self._media is None:
return None
media_type = self.server.library.getByKey(
self.media_content_id).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._media is not None:
total_time = self._media.get('duration')
return total_time
@property
def media_image_url(self):
""" Image url of current playing media. """
if self._media is not None:
return self.server.library.getByKey(self.media_content_id).thumbUrl
return None
@property
def media_title(self):
""" Title of current playing media. """
# find a string we can use as a title
if self._media is not None:
return self.server.library.getByKey(self.media_content_id).title
@property
def media_season(self):
""" Season of curent playing media. (TV Show only) """
if self._media is not None:
show_season = self.server.library.getByKey(
self.media_content_id).season().index
return show_season
return None
@property
def media_series_title(self):
""" Series title of current playing media. (TV Show only)"""
if self._media is not None:
series_title = self.server.library.getByKey(
self.media_content_id).show().title
return series_title
return None
@property
def media_episode(self):
""" Episode of current playing media. (TV Show only) """
if self._media is not None:
show_episode = self.server.library.getByKey(
self.media_content_id).index
return show_episode
return None
@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.client.play()
def media_pause(self):
""" media_pause media player. """
self.client.pause()
def media_next_track(self):
""" Send next track command. """
self.client.skipNext()
def media_previous_track(self):
""" Send previous track command. """
self.client.skipPrevious()

View File

@ -0,0 +1,206 @@
"""
homeassistant.components.media_player.sonos
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Provides an interface to Sonos players (via SoCo)
Configuration:
To use SoCo, add something like this to your configuration:
media_player:
platform: sonos
"""
import logging
import datetime
from homeassistant.components.media_player import (
MediaPlayerDevice, SUPPORT_PAUSE, SUPPORT_SEEK, SUPPORT_VOLUME_SET,
SUPPORT_VOLUME_MUTE, SUPPORT_PREVIOUS_TRACK, SUPPORT_NEXT_TRACK,
MEDIA_TYPE_MUSIC)
from homeassistant.const import (
STATE_IDLE, STATE_PLAYING, STATE_PAUSED, STATE_UNKNOWN)
REQUIREMENTS = ['SoCo==0.11.1']
_LOGGER = logging.getLogger(__name__)
# The soco library is excessively chatty when it comes to logging and
# causes a LOT of spam in the logs due to making a http connection to each
# speaker every 10 seconds. Quiet it down a bit to just actual problems.
_SOCO_LOGGER = logging.getLogger('soco')
_SOCO_LOGGER.setLevel(logging.ERROR)
_REQUESTS_LOGGER = logging.getLogger('requests')
_REQUESTS_LOGGER.setLevel(logging.ERROR)
SUPPORT_SONOS = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE |\
SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_SEEK
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
""" Sets up the Sonos platform. """
import soco
players = soco.discover()
if not players:
_LOGGER.warning('No Sonos speakers found. Disabling: %s', __name__)
return False
add_devices(SonosDevice(hass, p) for p in players)
_LOGGER.info('Added %s Sonos speakers', len(players))
return True
# pylint: disable=too-many-instance-attributes
# pylint: disable=too-many-public-methods
# pylint: disable=abstract-method
class SonosDevice(MediaPlayerDevice):
""" Represents a Sonos device. """
# pylint: disable=too-many-arguments
def __init__(self, hass, player):
self.hass = hass
super(SonosDevice, self).__init__()
self._player = player
self.update()
@property
def should_poll(self):
return True
def update_sonos(self, now):
""" Updates state, called by track_utc_time_change """
self.update_ha_state(True)
@property
def name(self):
""" Returns the name of the device. """
return self._name
@property
def unique_id(self):
""" Returns a unique id. """
return "{}.{}".format(self.__class__, self._player.uid)
@property
def state(self):
""" Returns the state of the device. """
if self._status == 'PAUSED_PLAYBACK':
return STATE_PAUSED
if self._status == 'PLAYING':
return STATE_PLAYING
if self._status == 'STOPPED':
return STATE_IDLE
return STATE_UNKNOWN
def update(self):
""" Retrieve latest state. """
self._name = self._player.get_speaker_info()['zone_name'].replace(
' (R)', '').replace(' (L)', '')
self._status = self._player.get_current_transport_info().get(
'current_transport_state')
self._trackinfo = self._player.get_current_track_info()
@property
def volume_level(self):
""" Volume level of the media player (0..1). """
return self._player.volume / 100.0
@property
def is_volume_muted(self):
return self._player.mute
@property
def media_content_id(self):
""" Content ID of current playing media. """
return self._trackinfo.get('title', None)
@property
def media_content_type(self):
""" Content type of current playing media. """
return MEDIA_TYPE_MUSIC
@property
def media_duration(self):
""" Duration of current playing media in seconds. """
dur = self._trackinfo.get('duration', '0:00')
# If the speaker is playing from the "line-in" source, getting
# track metadata can return NOT_IMPLEMENTED, which breaks the
# volume logic below
if dur == 'NOT_IMPLEMENTED':
return None
return sum(60 ** x[0] * int(x[1]) for x in
enumerate(reversed(dur.split(':'))))
@property
def media_image_url(self):
""" Image url of current playing media. """
if 'album_art' in self._trackinfo:
return self._trackinfo['album_art']
@property
def media_title(self):
""" Title of current playing media. """
if 'artist' in self._trackinfo and 'title' in self._trackinfo:
return '{artist} - {title}'.format(
artist=self._trackinfo['artist'],
title=self._trackinfo['title']
)
if 'title' in self._status:
return self._trackinfo['title']
@property
def supported_media_commands(self):
""" Flags of media commands that are supported. """
return SUPPORT_SONOS
def turn_off(self):
""" turn_off media player. """
self._player.pause()
def volume_up(self):
""" volume_up media player. """
self._player.volume += 1
def volume_down(self):
""" volume_down media player. """
self._player.volume -= 1
def set_volume_level(self, volume):
""" set volume level, range 0..1. """
self._player.volume = str(int(volume * 100))
def mute_volume(self, mute):
""" mute (true) or unmute (false) media player. """
self._player.mute = mute
def media_play(self):
""" media_play media player. """
self._player.play()
def media_pause(self):
""" media_pause media player. """
self._player.pause()
def media_next_track(self):
""" Send next track command. """
self._player.next()
def media_previous_track(self):
""" Send next track command. """
self._player.previous()
def media_seek(self, position):
""" Send seek command. """
self._player.seek(str(datetime.timedelta(seconds=int(position))))
def turn_on(self):
""" turn the media player on. """
self._player.play()

View File

@ -60,6 +60,7 @@ MQTT_CLIENT = None
DEFAULT_PORT = 1883
DEFAULT_KEEPALIVE = 60
DEFAULT_QOS = 0
SERVICE_PUBLISH = 'publish'
EVENT_MQTT_MESSAGE_RECEIVED = 'MQTT_MESSAGE_RECEIVED'
@ -79,17 +80,18 @@ ATTR_PAYLOAD = 'payload'
ATTR_QOS = 'qos'
def publish(hass, topic, payload, qos=0):
def publish(hass, topic, payload, qos=None):
""" Send an MQTT message. """
data = {
ATTR_TOPIC: topic,
ATTR_PAYLOAD: payload,
ATTR_QOS: qos,
}
if qos is not None:
data[ATTR_QOS] = qos
hass.services.call(DOMAIN, SERVICE_PUBLISH, data)
def subscribe(hass, topic, callback, qos=0):
def subscribe(hass, topic, callback, qos=DEFAULT_QOS):
""" Subscribe to a topic. """
def mqtt_topic_subscriber(event):
""" Match subscribed MQTT topic. """
@ -141,7 +143,7 @@ def setup(hass, config):
""" Handle MQTT publish service calls. """
msg_topic = call.data.get(ATTR_TOPIC)
payload = call.data.get(ATTR_PAYLOAD)
qos = call.data.get(ATTR_QOS)
qos = call.data.get(ATTR_QOS, DEFAULT_QOS)
if msg_topic is None or payload is None:
return
MQTT_CLIENT.publish(msg_topic, payload, qos)

View File

@ -256,7 +256,7 @@ class Recorder(threading.Thread):
""" Query the database. """
try:
with self.conn, self.lock:
_LOGGER.info("Running query %s", sql_query)
_LOGGER.debug("Running query %s", sql_query)
cur = self.conn.cursor()

View File

@ -1,137 +0,0 @@
"""
homeassistant.components.scheduler
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
A component that will act as a scheduler and perform actions based
on the events in the schedule.
It will read a json object from schedule.json in the config dir
and create a schedule based on it.
Each schedule is a JSON with the keys id, name, description,
entity_ids, and events.
- days is an array with the weekday number (monday=0) that the schedule
is active
- entity_ids an array with entity ids that the events in the schedule should
effect (can also be groups)
- events is an array of objects that describe the different events that is
supported. Read in the events descriptions for more information.
"""
import logging
import json
from homeassistant import bootstrap
from homeassistant.loader import get_component
from homeassistant.const import ATTR_ENTITY_ID
DOMAIN = 'scheduler'
DEPENDENCIES = []
_LOGGER = logging.getLogger(__name__)
_SCHEDULE_FILE = 'schedule.json'
def setup(hass, config):
""" Create the schedules. """
def setup_listener(schedule, event_data):
""" Creates the event listener based on event_data. """
event_type = event_data['type']
component = event_type
# if the event isn't part of a component
if event_type in ['time']:
component = 'scheduler.{}'.format(event_type)
elif not bootstrap.setup_component(hass, component, config):
_LOGGER.warn("Could setup event listener for %s", component)
return None
return get_component(component).create_event_listener(schedule,
event_data)
def setup_schedule(schedule_data):
""" Setup a schedule based on the description. """
schedule = Schedule(schedule_data['id'],
name=schedule_data['name'],
description=schedule_data['description'],
entity_ids=schedule_data['entity_ids'],
days=schedule_data['days'])
for event_data in schedule_data['events']:
event_listener = setup_listener(schedule, event_data)
if event_listener:
schedule.add_event_listener(event_listener)
schedule.schedule(hass)
return True
with open(hass.config.path(_SCHEDULE_FILE)) as schedule_file:
schedule_descriptions = json.load(schedule_file)
for schedule_description in schedule_descriptions:
if not setup_schedule(schedule_description):
return False
return True
class Schedule(object):
""" A Schedule """
# pylint: disable=too-many-arguments
def __init__(self, schedule_id, name=None, description=None,
entity_ids=None, days=None):
self.schedule_id = schedule_id
self.name = name
self.description = description
self.entity_ids = entity_ids or []
self.days = days or [0, 1, 2, 3, 4, 5, 6]
self.__event_listeners = []
def add_event_listener(self, event_listener):
""" Add a event to the schedule. """
self.__event_listeners.append(event_listener)
def schedule(self, hass):
""" Schedule all the events in the schedule. """
for event in self.__event_listeners:
event.schedule(hass)
class EventListener(object):
""" The base EventListener class that the schedule uses. """
def __init__(self, schedule):
self.my_schedule = schedule
def schedule(self, hass):
""" Schedule the event """
pass
def execute(self, hass):
""" execute the event """
pass
# pylint: disable=too-few-public-methods
class ServiceEventListener(EventListener):
""" A EventListener that calls a service when executed. """
def __init__(self, schdule, service):
EventListener.__init__(self, schdule)
(self.domain, self.service) = service.split('.')
def execute(self, hass):
""" Call the service. """
data = {ATTR_ENTITY_ID: self.my_schedule.entity_ids}
hass.services.call(self.domain, self.service, data)
# Reschedule for next day
self.schedule(hass)

View File

@ -1,70 +0,0 @@
"""
homeassistant.components.scheduler.time
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
An event in the scheduler component that will call the service
every specified day at the time specified.
A time event need to have the type 'time', which service to call and at
which time.
{
"type": "time",
"service": "switch.turn_off",
"time": "22:00:00"
}
"""
from datetime import timedelta
import logging
import homeassistant.util.dt as dt_util
from homeassistant.helpers.event import track_point_in_time
from homeassistant.components.scheduler import ServiceEventListener
_LOGGER = logging.getLogger(__name__)
def create_event_listener(schedule, event_listener_data):
""" Create a TimeEvent based on the description. """
service = event_listener_data['service']
(hour, minute, second) = [int(x) for x in
event_listener_data['time'].split(':')]
return TimeEventListener(schedule, service, hour, minute, second)
# pylint: disable=too-few-public-methods
class TimeEventListener(ServiceEventListener):
""" The time event that the scheduler uses. """
# pylint: disable=too-many-arguments
def __init__(self, schedule, service, hour, minute, second):
ServiceEventListener.__init__(self, schedule, service)
self.hour = hour
self.minute = minute
self.second = second
def schedule(self, hass):
""" Schedule this event so that it will be called. """
next_time = dt_util.now().replace(
hour=self.hour, minute=self.minute, second=self.second)
# Calculate the next time the event should be executed.
# That is the next day that the schedule is configured to run
while next_time < dt_util.now() or \
next_time.weekday() not in self.my_schedule.days:
next_time = next_time + timedelta(days=1)
# pylint: disable=unused-argument
def execute(now):
""" Call the execute method """
self.execute(hass)
track_point_in_time(hass, execute, next_time)
_LOGGER.info(
'TimeEventListener scheduled for %s, will call service %s.%s',
next_time, self.domain, self.service)

View File

@ -1,7 +1,7 @@
"""
homeassistant.components.script
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
entity_id
Scripts are a sequence of actions that can be triggered manually
by the user or automatically based upon automation events, etc.
"""
@ -22,7 +22,10 @@ CONF_ALIAS = "alias"
CONF_SERVICE = "execute_service"
CONF_SERVICE_DATA = "service_data"
CONF_SEQUENCE = "sequence"
CONF_EVENT = "event"
CONF_EVENT_DATA = "event_data"
CONF_DELAY = "delay"
ATTR_ENTITY_ID = "entity_id"
_LOGGER = logging.getLogger(__name__)
@ -41,15 +44,22 @@ def setup(hass, config):
hass.services.register(DOMAIN, name, script)
scripts.append(script)
def _get_entities(service):
""" Make sure that we always get a list of entities """
if isinstance(service.data[ATTR_ENTITY_ID], list):
return service.data[ATTR_ENTITY_ID]
else:
return [service.data[ATTR_ENTITY_ID]]
def turn_on(service):
""" Calls a script. """
for entity_id in service.data['entity_id']:
for entity_id in _get_entities(service):
domain, service = split_entity_id(entity_id)
hass.services.call(domain, service, {})
def turn_off(service):
""" Cancels a script. """
for entity_id in service.data['entity_id']:
for entity_id in _get_entities(service):
for script in scripts:
if script.entity_id == entity_id:
script.cancel()
@ -109,6 +119,8 @@ class Script(object):
for action in self.actions:
if CONF_SERVICE in action:
self._call_service(action)
elif CONF_EVENT in action:
self._fire_event(action)
elif CONF_DELAY in action:
delay = timedelta(**action[CONF_DELAY])
point_in_time = date_util.now() + delay
@ -140,3 +152,10 @@ class Script(object):
domain, service = split_entity_id(action[CONF_SERVICE])
data = action.get(CONF_SERVICE_DATA, {})
self.hass.services.call(domain, service, data)
def _fire_event(self, action):
""" Fires an event. """
self.last_action = action.get(CONF_ALIAS, action[CONF_EVENT])
_LOGGER.info("Executing script %s step %s", self.alias,
self.last_action)
self.hass.bus.fire(action[CONF_EVENT], action.get(CONF_EVENT_DATA))

View File

@ -65,7 +65,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
resource = config.get('resource', None)
try:
response = get(resource)
response = get(resource, timeout=10)
except exceptions.MissingSchema:
_LOGGER.error("Missing resource or schema in configuration. "
"Add http:// to your URL.")
@ -141,7 +141,7 @@ class ArestData(object):
def update(self):
""" Gets the latest data from aREST device. """
try:
response = get(self.resource)
response = get(self.resource, timeout=10)
if 'error' in self.data:
del self.data['error']
self.data = response.json()['variables']

View File

@ -0,0 +1,139 @@
"""
homeassistant.components.sensor.command_sensor
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Allows to configure custom shell commands to turn a value for a sensor.
Configuration:
To use the command_line sensor you will need to add something like the
following to your configuration.yaml file.
sensor:
platform: command_sensor
name: "Command sensor"
command: sensor_command
unit_of_measurement: "°C"
correction_factor: 0.0001
decimal_places: 0
Variables:
name
*Optional
Name of the command sensor.
command
*Required
The action to take to get the value.
unit_of_measurement
*Optional
Defines the units of measurement of the sensor, if any.
correction_factor
*Optional
A float value to do some basic calculations.
decimal_places
*Optional
Number of decimal places of the value. Default is 0.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.command_sensor.html
"""
import logging
import subprocess
from datetime import timedelta
from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = "Command Sensor"
# Return cached results if last scan was less then this time ago
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
""" Add the Command Sensor. """
if config.get('command') is None:
_LOGGER.error('Missing required variable: "command"')
return False
data = CommandSensorData(config.get('command'))
add_devices_callback([CommandSensor(
data,
config.get('name', DEFAULT_NAME),
config.get('unit_of_measurement'),
config.get('correction_factor', 1.0),
config.get('decimal_places', 0)
)])
# pylint: disable=too-many-arguments
class CommandSensor(Entity):
""" Represents a sensor that is returning a value of a shell commands. """
def __init__(self, data, name, unit_of_measurement, corr_factor,
decimal_places):
self.data = data
self._name = name
self._state = False
self._unit_of_measurement = unit_of_measurement
self._corr_factor = float(corr_factor)
self._decimal_places = decimal_places
self.update()
@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 and updates the state. """
self.data.update()
value = self.data.value
try:
if value is not None:
if self._corr_factor is not None:
self._state = round((float(value) * self._corr_factor),
self._decimal_places)
else:
self._state = value
except ValueError:
self._state = value
# pylint: disable=too-few-public-methods
class CommandSensorData(object):
""" Class for handling the data retrieval. """
def __init__(self, command):
self.command = command
self.value = None
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
""" Gets the latest data with a shell command. """
_LOGGER.info('Running command: %s', self.command)
try:
return_value = subprocess.check_output(self.command.split())
self.value = return_value.strip().decode('utf-8')
except subprocess.CalledProcessError:
_LOGGER.error('Command failed: %s', self.command)

View File

@ -0,0 +1,204 @@
"""
homeassistant.components.sensor.glances
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Gathers system information of hosts which running glances.
Configuration:
To use the glances sensor you will need to add something like the following
to your configuration.yaml file.
sensor:
platform: glances
name: Glances sensor
host: IP_ADDRESS
port: 61208
resources:
- 'disk_use_percent'
- 'disk_use'
- 'disk_free'
- 'memory_use_percent'
- 'memory_use'
- 'memory_free'
- 'swap_use_percent'
- 'swap_use'
- 'swap_free'
- 'processor_load'
- 'process_running'
- 'process_total'
- 'process_thread'
- 'process_sleeping'
Variables:
name
*Optional
The name of the sensor. Default is 'Glances Sensor'.
host
*Required
The IP address of your host, e.g. 192.168.1.32.
port
*Optional
The network port to connect to. Default is 61208.
resources
*Required
Resources to monitor on the host. See the configuration example above for a
list of all available conditions to monitor.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.glances.html
"""
import logging
import requests
from datetime import timedelta
from homeassistant.util import Throttle
from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = 'Glances Sensor'
_RESOURCE = '/api/2/all'
SENSOR_TYPES = {
'disk_use_percent': ['Disk Use', '%'],
'disk_use': ['Disk Use', 'GiB'],
'disk_free': ['Disk Free', 'GiB'],
'memory_use_percent': ['RAM Use', '%'],
'memory_use': ['RAM Use', 'MiB'],
'memory_free': ['RAM Free', 'MiB'],
'swap_use_percent': ['Swap Use', '%'],
'swap_use': ['Swap Use', 'GiB'],
'swap_free': ['Swap Free', 'GiB'],
'processor_load': ['CPU Load', ''],
'process_running': ['Running', ''],
'process_total': ['Total', ''],
'process_thread': ['Thread', ''],
'process_sleeping': ['Sleeping', '']
}
_LOGGER = logging.getLogger(__name__)
# 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):
""" Setup the Glances sensor. """
if not config.get('host'):
_LOGGER.error('"host:" is missing your configuration')
return False
host = config.get('host')
port = config.get('port', 61208)
url = 'http://{}:{}{}'.format(host, port, _RESOURCE)
try:
response = requests.get(url, timeout=10)
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. '
'Please heck our details in the configuration file.')
return False
except requests.exceptions.ConnectionError:
_LOGGER.error('No route to resource/endpoint. '
'Please check the details in the configuration file.')
return False
rest = GlancesData(url)
dev = []
for resource in config['resources']:
if resource not in SENSOR_TYPES:
_LOGGER.error('Sensor type: "%s" does not exist', resource)
else:
dev.append(GlancesSensor(rest, resource))
add_devices(dev)
class GlancesSensor(Entity):
""" Implements a REST sensor. """
def __init__(self, rest, sensor_type):
self.rest = rest
self._name = SENSOR_TYPES[sensor_type][0]
self.type = sensor_type
self._state = None
self._unit_of_measurement = SENSOR_TYPES[sensor_type][1]
self.update()
@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
# pylint: disable=too-many-branches
def update(self):
""" Gets the latest data from REST API and updates the state. """
self.rest.update()
value = self.rest.data
if value is not None:
if self.type == 'disk_use_percent':
self._state = value['fs'][0]['percent']
elif self.type == 'disk_use':
self._state = round(value['fs'][0]['used'] / 1024**3, 1)
elif self.type == 'disk_free':
self._state = round(value['fs'][0]['free'] / 1024**3, 1)
elif self.type == 'memory_use_percent':
self._state = value['mem']['percent']
elif self.type == 'memory_use':
self._state = round(value['mem']['used'] / 1024**2, 1)
elif self.type == 'memory_free':
self._state = round(value['mem']['free'] / 1024**2, 1)
elif self.type == 'swap_use_percent':
self._state = value['memswap']['percent']
elif self.type == 'swap_use':
self._state = round(value['memswap']['used'] / 1024**3, 1)
elif self.type == 'swap_free':
self._state = round(value['memswap']['free'] / 1024**3, 1)
elif self.type == 'processor_load':
self._state = value['load']['min15']
elif self.type == 'process_running':
self._state = value['processcount']['running']
elif self.type == 'process_total':
self._state = value['processcount']['total']
elif self.type == 'process_thread':
self._state = value['processcount']['thread']
elif self.type == 'process_sleeping':
self._state = value['processcount']['sleeping']
# pylint: disable=too-few-public-methods
class GlancesData(object):
""" Class for handling the data retrieval. """
def __init__(self, resource):
self.resource = resource
self.data = dict()
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
""" Gets the latest data from REST service. """
try:
response = requests.get(self.resource, timeout=10)
self.data = response.json()
except requests.exceptions.ConnectionError:
_LOGGER.error("No route to host/endpoint.")
self.data = None

View File

@ -91,7 +91,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
_LOGGER.error(
"Connection error "
"Please check your settings for OpenWeatherMap.")
return None
return False
data = WeatherData(owm, forecast, hass.config.latitude,
hass.config.longitude)

View File

@ -31,7 +31,7 @@ Details for the API : http://transport.opendata.ch
"""
import logging
from datetime import timedelta
from requests import get
import requests
from homeassistant.util import Throttle
import homeassistant.util.dt as dt_util
@ -53,7 +53,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
try:
for location in [config.get('from', None), config.get('to', None)]:
# transport.opendata.ch doesn't play nice with requests.Session
result = get(_RESOURCE + 'locations?query=%s' % location)
result = requests.get(_RESOURCE + 'locations?query=%s' % location,
timeout=10)
journey.append(result.json()['stations'][0]['name'])
except KeyError:
_LOGGER.exception(
@ -109,14 +110,14 @@ class PublicTransportData(object):
def update(self):
""" Gets the latest data from opendata.ch. """
response = get(
response = requests.get(
_RESOURCE +
'connections?' +
'from=' + self.start + '&' +
'to=' + self.destination + '&' +
'fields[]=connections/from/departureTimestamp/&' +
'fields[]=connections/')
'fields[]=connections/',
timeout=10)
connections = response.json()['connections'][:2]
try:

View File

@ -59,7 +59,6 @@ arg
Additional details for the type, eg. path, binary name, etc.
"""
import logging
import psutil
import homeassistant.util.dt as dt_util
from homeassistant.helpers.entity import Entity
@ -120,7 +119,7 @@ class SystemMonitorSensor(Entity):
@property
def name(self):
return self._name
return self._name.rstrip()
@property
def state(self):
@ -133,6 +132,7 @@ class SystemMonitorSensor(Entity):
# pylint: disable=too-many-branches
def update(self):
import psutil
if self.type == 'disk_use_percent':
self._state = psutil.disk_usage(self.argument).percent
elif self.type == 'disk_use':

View File

@ -36,12 +36,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
hasattr(value, 'humidity') and value.humidity
])
sensors.extend([
VerisureAlarm(value)
for value in verisure.get_alarm_status().values()
if verisure.SHOW_ALARM
])
add_devices(sensors)
@ -103,25 +97,3 @@ class VerisureHygrometer(Entity):
def update(self):
''' update sensor '''
verisure.update()
class VerisureAlarm(Entity):
""" represents a Verisure alarm status within home assistant. """
def __init__(self, alarm_status):
self._id = alarm_status.id
self._device = verisure.MY_PAGES.DEVICE_ALARM
@property
def name(self):
""" Returns the name of the device. """
return 'Alarm {}'.format(self._id)
@property
def state(self):
""" Returns the state of the device. """
return verisure.STATUS[self._device][self._id].label
def update(self):
''' update sensor '''
verisure.update()

View File

@ -25,10 +25,8 @@ import urllib
import homeassistant.util as util
import homeassistant.util.dt as dt_util
from homeassistant.helpers.event import (
track_point_in_utc_time, track_point_in_time)
from homeassistant.helpers.event import track_point_in_utc_time
from homeassistant.helpers.entity import Entity
from homeassistant.components.scheduler import ServiceEventListener
DEPENDENCIES = []
REQUIREMENTS = ['astral==0.8.1']
@ -214,95 +212,3 @@ class Sun(Entity):
track_point_in_utc_time(
self.hass, self.point_in_time_listener,
self.next_change + timedelta(seconds=1))
def create_event_listener(schedule, event_listener_data):
""" Create a sun event listener based on the description. """
negative_offset = False
service = event_listener_data['service']
offset_str = event_listener_data['offset']
event = event_listener_data['event']
if offset_str.startswith('-'):
negative_offset = True
offset_str = offset_str[1:]
(hour, minute, second) = [int(x) for x in offset_str.split(':')]
offset = timedelta(hours=hour, minutes=minute, seconds=second)
if event == 'sunset':
return SunsetEventListener(schedule, service, offset, negative_offset)
return SunriseEventListener(schedule, service, offset, negative_offset)
# pylint: disable=too-few-public-methods
class SunEventListener(ServiceEventListener):
""" This is the base class for sun event listeners. """
def __init__(self, schedule, service, offset, negative_offset):
ServiceEventListener.__init__(self, schedule, service)
self.offset = offset
self.negative_offset = negative_offset
def __get_next_time(self, next_event):
"""
Returns when the next time the service should be called.
Taking into account the offset and which days the event should execute.
"""
if self.negative_offset:
next_time = next_event - self.offset
else:
next_time = next_event + self.offset
while next_time < dt_util.now() or \
next_time.weekday() not in self.my_schedule.days:
next_time = next_time + timedelta(days=1)
return next_time
def schedule_next_event(self, hass, next_event):
""" Schedule the event. """
next_time = self.__get_next_time(next_event)
# pylint: disable=unused-argument
def execute(now):
""" Call the execute method. """
self.execute(hass)
track_point_in_time(hass, execute, next_time)
return next_time
# pylint: disable=too-few-public-methods
class SunsetEventListener(SunEventListener):
""" This class is used the call a service when the sun sets. """
def schedule(self, hass):
""" Schedule the event """
next_setting_dt = next_setting(hass)
next_time_dt = self.schedule_next_event(hass, next_setting_dt)
_LOGGER.info(
'SunsetEventListener scheduled for %s, will call service %s.%s',
next_time_dt, self.domain, self.service)
# pylint: disable=too-few-public-methods
class SunriseEventListener(SunEventListener):
""" This class is used the call a service when the sun rises. """
def schedule(self, hass):
""" Schedule the event. """
next_rising_dt = next_rising(hass)
next_time_dt = self.schedule_next_event(hass, next_rising_dt)
_LOGGER.info(
'SunriseEventListener scheduled for %s, will call service %s.%s',
next_time_dt, self.domain, self.service)

View File

@ -1,7 +1,7 @@
"""
homeassistant.components.switch.arduino
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Support for switching Arduino pins on and off. So fare only digital pins are
Support for switching Arduino pins on and off. So far only digital pins are
supported.
Configuration:

View File

@ -0,0 +1,123 @@
"""
homeassistant.components.switch.arest
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The arest switch can control the digital pins of a device running with the
aREST RESTful framework for Arduino, the ESP8266, and the Raspberry Pi.
Only tested with Arduino boards so far.
Configuration:
To use the arest switch you will need to add something like the following
to your configuration.yaml file.
sensor:
platform: arest
resource: http://IP_ADDRESS
pins:
11:
name: Fan Office
12:
name: Light Desk
Variables:
resource:
*Required
IP address of the device that is exposing an aREST API.
pins:
The number of the digital pin to switch.
These are the variables for the pins array:
name
*Required
The name for the pin that will be used in the frontend.
Details for the API: http://arest.io
"""
import logging
from requests import get, exceptions
from homeassistant.components.switch import SwitchDevice
from homeassistant.const import DEVICE_DEFAULT_NAME
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None):
""" Get the aREST switches. """
resource = config.get('resource', None)
try:
response = get(resource, timeout=10)
except exceptions.MissingSchema:
_LOGGER.error("Missing resource or schema in configuration. "
"Add http:// to your URL.")
return False
except exceptions.ConnectionError:
_LOGGER.error("No route to device. "
"Please check the IP address in the configuration file.")
return False
dev = []
pins = config.get('pins')
for pinnum, pin in pins.items():
dev.append(ArestSwitch(resource,
response.json()['name'],
pin.get('name'),
pinnum))
add_devices(dev)
class ArestSwitch(SwitchDevice):
""" Implements an aREST switch. """
def __init__(self, resource, location, name, pin):
self._resource = resource
self._name = '{} {}'.format(location.title(), name.title()) \
or DEVICE_DEFAULT_NAME
self._pin = pin
self._state = None
request = get('{}/mode/{}/o'.format(self._resource, self._pin),
timeout=10)
if request.status_code is not 200:
_LOGGER.error("Can't set mode. Is device offline?")
@property
def name(self):
""" The name of the switch. """
return self._name
@property
def is_on(self):
""" True if device is on. """
return self._state
def turn_on(self, **kwargs):
""" Turn the device on. """
request = get('{}/digital/{}/1'.format(self._resource, self._pin),
timeout=10)
if request.status_code == 200:
self._state = True
else:
_LOGGER.error("Can't turn on pin %s at %s. Is device offline?",
self._resource, self._pin)
def turn_off(self, **kwargs):
""" Turn the device off. """
request = get('{}/digital/{}/0'.format(self._resource, self._pin),
timeout=10)
if request.status_code == 200:
self._state = False
else:
_LOGGER.error("Can't turn off pin %s at %s. Is device offline?",
self._resource, self._pin)
def update(self):
""" Gets the latest data from aREST API and updates the state. """
request = get('{}/digital/{}'.format(self._resource, self._pin),
timeout=10)
self._state = request.json()['return_value'] != 0

View File

@ -2,13 +2,44 @@
"""
homeassistant.components.switch.command_switch
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Allows to configure custom shell commands to turn a switch on/off.
Configuration:
To use the command_line switch you will need to add something like the
following to your configuration.yaml file.
switch:
platform: command_switch
switches:
name_of_the_switch:
oncmd: switch_command on for name_of_the_switch
offcmd: switch_command off for name_of_the_switch
Variables:
These are the variables for the switches array:
name_of_the_switch
*Required
Name of the command switch. Multiple entries are possible.
oncmd
*Required
The action to take for on.
offcmd
*Required
The action to take for off.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/switch.command_switch.html
"""
import logging
from homeassistant.components.switch import SwitchDevice
import subprocess
from homeassistant.components.switch import SwitchDevice
_LOGGER = logging.getLogger(__name__)
@ -22,7 +53,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
for dev_name, properties in switches.items():
devices.append(
CommandSwitch(
dev_name,
properties.get('name', dev_name),
properties.get('oncmd', 'true'),
properties.get('offcmd', 'true')))

View File

@ -11,11 +11,10 @@ signal_repetitions: 3
"""
import logging
from homeassistant.const import ATTR_FRIENDLY_NAME
from homeassistant.helpers.entity import ToggleEntity
import tellcore.constants as tellcore_constants
from tellcore.library import DirectCallbackDispatcher
SINGAL_REPETITIONS = 1
REQUIREMENTS = ['tellcore-py==1.0.4']
@ -31,16 +30,31 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
"Failed to import tellcore")
return
# pylint: disable=no-member
if telldus.TelldusCore.callback_dispatcher is None:
dispatcher = DirectCallbackDispatcher()
core = telldus.TelldusCore(callback_dispatcher=dispatcher)
else:
core = telldus.TelldusCore()
signal_repetitions = config.get('signal_repetitions', SINGAL_REPETITIONS)
core = telldus.TelldusCore()
switches_and_lights = core.devices()
switches = []
for switch in switches_and_lights:
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(True)
core.register_device_event(_device_event_callback)
add_devices_callback(switches)
@ -50,15 +64,20 @@ class TellstickSwitchDevice(ToggleEntity):
last_sent_command_mask = (tellcore_constants.TELLSTICK_TURNON |
tellcore_constants.TELLSTICK_TURNOFF)
def __init__(self, tellstick, signal_repetitions):
self.tellstick = tellstick
self.state_attr = {ATTR_FRIENDLY_NAME: tellstick.name}
def __init__(self, tellstick_device, signal_repetitions):
self.tellstick_device = tellstick_device
self.state_attr = {ATTR_FRIENDLY_NAME: tellstick_device.name}
self.signal_repetitions = signal_repetitions
@property
def should_poll(self):
""" Tells Home Assistant not to poll this entity. """
return False
@property
def name(self):
""" Returns the name of the switch if any. """
return self.tellstick.name
return self.tellstick_device.name
@property
def state_attributes(self):
@ -68,7 +87,7 @@ class TellstickSwitchDevice(ToggleEntity):
@property
def is_on(self):
""" 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)
return last_command == tellcore_constants.TELLSTICK_TURNON
@ -76,9 +95,11 @@ class TellstickSwitchDevice(ToggleEntity):
def turn_on(self, **kwargs):
""" Turns the switch on. """
for _ in range(self.signal_repetitions):
self.tellstick.turn_on()
self.tellstick_device.turn_on()
self.update_ha_state()
def turn_off(self, **kwargs):
""" Turns the switch off. """
for _ in range(self.signal_repetitions):
self.tellstick.turn_off()
self.tellstick_device.turn_off()
self.update_ha_state()

View File

@ -23,10 +23,17 @@ SCAN_INTERVAL = 60
SERVICE_SET_AWAY_MODE = "set_away_mode"
SERVICE_SET_TEMPERATURE = "set_temperature"
STATE_HEAT = "heat"
STATE_COOL = "cool"
STATE_IDLE = "idle"
ATTR_CURRENT_TEMPERATURE = "current_temperature"
ATTR_AWAY_MODE = "away_mode"
ATTR_MAX_TEMP = "max_temp"
ATTR_MIN_TEMP = "min_temp"
ATTR_TEMPERATURE_LOW = "target_temp_low"
ATTR_TEMPERATURE_HIGH = "target_temp_high"
ATTR_OPERATION = "current_operation"
_LOGGER = logging.getLogger(__name__)
@ -126,19 +133,25 @@ class ThermostatDevice(Entity):
user_unit = self.hass.config.temperature_unit
data = {
ATTR_CURRENT_TEMPERATURE: round(convert(self.current_temperature,
thermostat_unit,
user_unit), 1),
ATTR_MIN_TEMP: round(convert(self.min_temp,
thermostat_unit,
user_unit), 0),
ATTR_MAX_TEMP: round(convert(self.max_temp,
thermostat_unit,
user_unit), 0)
ATTR_CURRENT_TEMPERATURE: round(convert(
self.current_temperature, thermostat_unit, user_unit), 1),
ATTR_MIN_TEMP: round(convert(
self.min_temp, thermostat_unit, user_unit), 0),
ATTR_MAX_TEMP: round(convert(
self.max_temp, thermostat_unit, user_unit), 0),
ATTR_TEMPERATURE: round(convert(
self.target_temperature, thermostat_unit, user_unit), 0),
ATTR_TEMPERATURE_LOW: round(convert(
self.target_temperature_low, thermostat_unit, user_unit), 0),
ATTR_TEMPERATURE_HIGH: round(convert(
self.target_temperature_high, thermostat_unit, user_unit), 0),
}
is_away = self.is_away_mode_on
operation = self.operation
if operation is not None:
data[ATTR_OPERATION] = operation
is_away = self.is_away_mode_on
if is_away is not None:
data[ATTR_AWAY_MODE] = STATE_ON if is_away else STATE_OFF
@ -152,18 +165,33 @@ class ThermostatDevice(Entity):
@property
def unit_of_measurement(self):
""" Unit of measurement this thermostat expresses itself in. """
return NotImplementedError
raise NotImplementedError
@property
def current_temperature(self):
""" Returns the current temperature. """
raise NotImplementedError
@property
def operation(self):
""" Returns current operation ie. heat, cool, idle """
return None
@property
def target_temperature(self):
""" Returns the temperature we try to reach. """
raise NotImplementedError
@property
def target_temperature_low(self):
""" Returns the lower bound temperature we try to reach. """
return self.target_temperature
@property
def target_temperature_high(self):
""" Returns the upper bound temperature we try to reach. """
return self.target_temperature
@property
def is_away_mode_on(self):
"""

View File

@ -3,9 +3,11 @@ homeassistant.components.thermostat.nest
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Adds support for Nest thermostats.
"""
import socket
import logging
from homeassistant.components.thermostat import ThermostatDevice
from homeassistant.components.thermostat import (ThermostatDevice, STATE_COOL,
STATE_IDLE, STATE_HEAT)
from homeassistant.const import (CONF_USERNAME, CONF_PASSWORD, TEMP_CELCIUS)
REQUIREMENTS = ['python-nest==2.6.0']
@ -34,12 +36,16 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
return
napi = nest.Nest(username, password)
add_devices([
NestThermostat(structure, device)
for structure in napi.structures
for device in structure.devices
])
try:
add_devices([
NestThermostat(structure, device)
for structure in napi.structures
for device in structure.devices
])
except socket.error:
logger.error(
"Connection error logging into the nest web service"
)
class NestThermostat(ThermostatDevice):
@ -83,25 +89,52 @@ class NestThermostat(ThermostatDevice):
""" Returns the current temperature. """
return round(self.device.temperature, 1)
@property
def operation(self):
""" Returns current operation ie. heat, cool, idle """
if self.device.hvac_ac_state is True:
return STATE_COOL
elif self.device.hvac_heater_state is True:
return STATE_HEAT
else:
return STATE_IDLE
@property
def target_temperature(self):
""" Returns the temperature we try to reach. """
target = self.device.target
if isinstance(target, tuple):
if self.device.mode == 'range':
low, high = target
if self.current_temperature < low:
temp = low
elif self.current_temperature > high:
if self.operation == STATE_COOL:
temp = high
elif self.operation == STATE_HEAT:
temp = low
else:
temp = (low + high)/2
range_average = (low + high)/2
if self.current_temperature < range_average:
temp = low
elif self.current_temperature >= range_average:
temp = high
else:
temp = target
return round(temp, 1)
@property
def target_temperature_low(self):
""" Returns the lower bound temperature we try to reach. """
if self.device.mode == 'range':
return round(self.device.target[0], 1)
return round(self.target_temperature, 1)
@property
def target_temperature_high(self):
""" Returns the upper bound temperature we try to reach. """
if self.device.mode == 'range':
return round(self.device.target[1], 1)
return round(self.target_temperature, 1)
@property
def is_away_mode_on(self):
""" Returns if away mode is on. """
@ -109,6 +142,11 @@ class NestThermostat(ThermostatDevice):
def set_temperature(self, temperature):
""" Set new target temperature """
if self.device.mode == 'range':
if self.target_temperature == self.target_temperature_low:
temperature = (temperature, self.target_temperature_high)
elif self.target_temperature == self.target_temperature_high:
temperature = (self.target_temperature_low, temperature)
self.device.target = temperature
def turn_away_mode_on(self):

View File

@ -59,8 +59,9 @@ from homeassistant.const import (
DOMAIN = "verisure"
DISCOVER_SENSORS = 'verisure.sensors'
DISCOVER_SWITCHES = 'verisure.switches'
DISCOVER_ALARMS = 'verisure.alarm_control_panel'
DEPENDENCIES = []
DEPENDENCIES = ['alarm_control_panel']
REQUIREMENTS = [
'https://github.com/persandstrom/python-verisure/archive/'
'9873c4527f01b1ba1f72ae60f7f35854390d59be.zip#python-verisure==0.2.6'
@ -123,7 +124,8 @@ def setup(hass, config):
# Load components for the devices in the ISY controller that we support
for comp_name, discovery in ((('sensor', DISCOVER_SENSORS),
('switch', DISCOVER_SWITCHES))):
('switch', DISCOVER_SWITCHES),
('alarm_control_panel', DISCOVER_ALARMS))):
component = get_component(comp_name)
_LOGGER.info(config[DOMAIN])
bootstrap.setup_component(hass, component.DOMAIN, config)
@ -166,7 +168,7 @@ def reconnect():
def update():
""" Updates the status of verisure components. """
if WRONG_PASSWORD_GIVEN:
# Is there any way to inform user?
_LOGGER.error('Wrong password')
return
try:

View File

@ -1,6 +1,6 @@
""" Constants used by Home Assistant components. """
__version__ = "0.7.2"
__version__ = "0.7.4dev0"
# Can be used to specify a catch all when registering state or event listeners.
MATCH_ALL = '*'
@ -40,13 +40,16 @@ STATE_ON = 'on'
STATE_OFF = 'off'
STATE_HOME = 'home'
STATE_NOT_HOME = 'not_home'
STATE_UNKNOWN = "unknown"
STATE_UNKNOWN = 'unknown'
STATE_OPEN = 'open'
STATE_CLOSED = 'closed'
STATE_PLAYING = 'playing'
STATE_PAUSED = 'paused'
STATE_IDLE = 'idle'
STATE_STANDBY = 'standby'
STATE_ALARM_DISARMED = 'disarmed'
STATE_ALARM_ARMED_HOME = 'armed_home'
STATE_ALARM_ARMED_AWAY = 'armed_away'
# #### STATE AND EVENT ATTRIBUTES ####
# Contains current time for a TIME_CHANGED event
@ -114,6 +117,10 @@ SERVICE_MEDIA_NEXT_TRACK = "media_next_track"
SERVICE_MEDIA_PREVIOUS_TRACK = "media_previous_track"
SERVICE_MEDIA_SEEK = "media_seek"
SERVICE_ALARM_DISARM = "alarm_disarm"
SERVICE_ALARM_ARM_HOME = "alarm_arm_home"
SERVICE_ALARM_ARM_AWAY = "alarm_arm_away"
# #### API / REMOTE ####
SERVER_PORT = 8123

View File

@ -26,6 +26,7 @@ from homeassistant.exceptions import (
HomeAssistantError, InvalidEntityFormatError)
import homeassistant.util as util
import homeassistant.util.dt as date_util
import homeassistant.util.location as location
import homeassistant.helpers.temperature as temp_helper
from homeassistant.config import get_default_config_dir
@ -676,6 +677,10 @@ class Config(object):
# Directory that holds the configuration
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):
""" Returns path to the file within the config dir. """
return os.path.join(self.config_dir, *path)

View File

@ -10,8 +10,8 @@ from collections import defaultdict
from homeassistant.exceptions import NoEntitySpecifiedError
from homeassistant.const import (
ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT, ATTR_HIDDEN,
STATE_ON, STATE_OFF, DEVICE_DEFAULT_NAME, TEMP_CELCIUS,
ATTR_FRIENDLY_NAME, ATTR_HIDDEN, ATTR_UNIT_OF_MEASUREMENT,
DEVICE_DEFAULT_NAME, STATE_ON, STATE_OFF, STATE_UNKNOWN, TEMP_CELCIUS,
TEMP_FAHRENHEIT)
# Dict mapping entity_id to a boolean that overwrites the hidden property
@ -44,17 +44,17 @@ class Entity(object):
@property
def name(self):
""" Returns the name of the entity. """
return self.get_name()
return DEVICE_DEFAULT_NAME
@property
def state(self):
""" Returns the state of the entity. """
return self.get_state()
return STATE_UNKNOWN
@property
def state_attributes(self):
""" Returns the state attributes. """
return {}
return None
@property
def unit_of_measurement(self):
@ -64,34 +64,12 @@ class Entity(object):
@property
def hidden(self):
""" Suggestion if the entity should be hidden from UIs. """
return self._hidden
@hidden.setter
def hidden(self, val):
""" Sets the suggestion for visibility. """
self._hidden = bool(val)
return False
def update(self):
""" Retrieve latest state. """
pass
# DEPRECATION NOTICE:
# Device is moving from getters to properties.
# For now the new properties will call the old functions
# This will be removed in the future.
def get_name(self):
""" Returns the name of the entity if any. """
return DEVICE_DEFAULT_NAME
def get_state(self):
""" Returns state of the entity. """
return "Unknown"
def get_state_attributes(self):
""" Returns optional state attributes. """
return None
# DO NOT OVERWRITE
# These properties and methods are either managed by Home Assistant or they
# are used to perform a very specific function. Overwriting these may

View File

@ -129,13 +129,13 @@ class EntityComponent(object):
if platform is None:
return
platform_name = '{}.{}'.format(self.domain, platform_type)
try:
platform.setup_platform(
self.hass, platform_config, self.add_entities, discovery_info)
self.hass.config.components.append(platform_name)
except Exception: # pylint: disable=broad-except
self.logger.exception(
'Error while setting up platform %s', platform_type)
return
platform_name = '{}.{}'.format(self.domain, platform_type)
self.hass.config.components.append(platform_name)

View File

@ -51,6 +51,8 @@ def reproduce_state(hass, states, blocking=False):
current_state = hass.states.get(state.entity_id)
if current_state is None:
_LOGGER.warning('reproduce_state: Unable to find entity %s',
state.entity_id)
continue
if state.state == STATE_ON:
@ -58,7 +60,8 @@ def reproduce_state(hass, states, blocking=False):
elif state.state == STATE_OFF:
service = SERVICE_TURN_OFF
else:
_LOGGER.warning("Unable to reproduce state for %s", state)
_LOGGER.warning("reproduce_state: Unable to reproduce state %s",
state)
continue
service_data = dict(state.attributes)

View File

View File

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>org.homeassitant</string>
<key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
<string>/usr/local/bin/:/usr/bin:$PATH</string>
</dict>
<key>Program</key>
<string>$HASS_PATH$</string>
<key>AbandonProcessGroup</key>
<false/>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<dict>
<key>SuccessfulExit</key>
<false/>
</dict>
<key>StandardErrorPath</key>
<string>/Users/$USER$/Library/Logs/homeassitant.log</string>
<key>StandardOutPath</key>
<string>/Users/$USER$/Library/Logs/homeassitant.log</string>
</dict>
</plist>

View File

@ -21,7 +21,7 @@ from .dt import datetime_to_local_str, utcnow
RE_SANITIZE_FILENAME = re.compile(r'(~|\.\.|/|\\)')
RE_SANITIZE_PATH = re.compile(r'(~|\.(\.)+)')
RE_SLUGIFY = re.compile(r'[^A-Za-z0-9_]+')
RE_SLUGIFY = re.compile(r'[^a-z0-9_]+')
def sanitize_filename(filename):
@ -36,7 +36,7 @@ def sanitize_path(path):
def slugify(text):
""" Slugifies a given text. """
text = text.replace(" ", "_")
text = text.lower().replace(" ", "_")
return RE_SLUGIFY.sub("", text)
@ -71,7 +71,7 @@ def ensure_unique_string(preferred_string, current_strings):
""" Returns a string that is not present in current_strings.
If preferred string exists will append _2, _3, .. """
test_string = preferred_string
current_strings = list(current_strings)
current_strings = set(current_strings)
tries = 1
@ -244,22 +244,22 @@ class Throttle(object):
Wrapper that allows wrapped to be called only once per min_time.
If we cannot acquire the lock, it is running so return None.
"""
if lock.acquire(False):
try:
last_call = wrapper.last_call
if not lock.acquire(False):
return None
try:
last_call = wrapper.last_call
# Check if method is never called or no_throttle is given
force = not last_call or kwargs.pop('no_throttle', False)
# Check if method is never called or no_throttle is given
force = not last_call or kwargs.pop('no_throttle', False)
if force or datetime.now() - last_call > self.min_time:
result = method(*args, **kwargs)
wrapper.last_call = datetime.now()
return result
else:
return None
finally:
lock.release()
if force or utcnow() - last_call > self.min_time:
result = method(*args, **kwargs)
wrapper.last_call = utcnow()
return result
else:
return None
finally:
lock.release()
wrapper.last_call = None

View File

@ -131,3 +131,20 @@ def date_str_to_date(dt_str):
def strip_microseconds(dattim):
""" Returns a copy of dattime object but with microsecond set to 0. """
return dattim.replace(microsecond=0)
def parse_time_str(time_str):
""" Parse a time string (00:20:00) into Time object.
Return None if invalid.
"""
parts = str(time_str).split(':')
if len(parts) < 2:
return None
try:
hour = int(parts[0])
minute = int(parts[1])
second = int(parts[2]) if len(parts) > 2 else 0
return dt.time(hour, minute, second)
except ValueError:
# ValueError if value cannot be converted to an int or not in range
return None

View File

@ -1,5 +1,6 @@
"""Module with location helpers."""
import collections
from math import radians, cos, sin, asin, sqrt
import requests
@ -28,3 +29,20 @@ def detect_location_info():
'BS', 'BZ', 'KY', 'PW', 'US', 'AS', 'VI')
return LocationInfo(**data)
# From: http://stackoverflow.com/a/4913653/646416
def distance(lon1, lat1, lon2, lat2):
"""
Calculate the great circle distance in meters between two points specified
in decimal degrees on the earth using the Haversine algorithm.
"""
# convert decimal degrees to radians
lon1, lat1, lon2, lat2 = (radians(val) for val in (lon1, lat1, lon2, lat2))
dlon = lon2 - lon1
dlat = lat2 - lat1
angle = sin(dlat/2)**2 + cos(lat1) * cos(lat2) * sin(dlon/2)**2
# Radius of earth in meters.
radius = 6371000
return 2 * radius * asin(sqrt(angle))

View File

@ -1,6 +1,6 @@
"""Helpers to install PyPi packages."""
import os
import logging
import os
import pkg_resources
import subprocess
import sys
@ -15,25 +15,24 @@ def install_package(package, upgrade=True, target=None):
"""Install a package on PyPi. Accepts pip compatible package strings.
Return boolean if install successfull."""
# Not using 'import pip; pip.main([])' because it breaks the logger
args = [sys.executable, '-m', 'pip', 'install', '--quiet', package]
if upgrade:
args.append('--upgrade')
if target:
args += ['--target', os.path.abspath(target)]
with INSTALL_LOCK:
if check_package_exists(package, target):
return True
_LOGGER.info('Attempting install of %s', package)
args = [sys.executable, '-m', 'pip', 'install', '--quiet', package]
if upgrade:
args.append('--upgrade')
if target:
args += ['--target', os.path.abspath(target)]
try:
return 0 == subprocess.call(args)
except subprocess.SubprocessError:
return False
def check_package_exists(package, target=None):
def check_package_exists(package, target):
"""Check if a package exists.
Returns True when the requirement is met.
Returns False when the package is not installed or doesn't meet req."""
@ -43,16 +42,5 @@ def check_package_exists(package, target=None):
# This is a zip file
req = pkg_resources.Requirement.parse(urlparse(package).fragment)
if target:
work_set = pkg_resources.WorkingSet([target])
search_fun = work_set.find
else:
search_fun = pkg_resources.get_distribution
try:
result = search_fun(req)
except (pkg_resources.DistributionNotFound, pkg_resources.VersionConflict):
return False
return bool(result)
return any(dist in req for dist in
pkg_resources.find_distributions(target))

2
pytest.ini Normal file
View File

@ -0,0 +1,2 @@
[pytest]
testpaths = tests

View File

@ -25,7 +25,7 @@ pyuserinput==0.1.9
tellcore-py==1.0.4
# Nmap bindings (device_tracker.nmap)
python-nmap==0.4.1
python-nmap==0.4.3
# PushBullet bindings (notify.pushbullet)
pushbullet.py==0.7.1
@ -86,7 +86,7 @@ https://github.com/theolind/pymysensors/archive/35b87d880147a34107da0d40cb815d75
pynetgear==0.3
# Netdisco (discovery)
netdisco==0.3
netdisco==0.4.1
# Wemo (switch.wemo)
pywemo==0.3
@ -130,3 +130,9 @@ https://github.com/balloob/home-assistant-nzb-clients/archive/616cad591540925992
# sensor.vera
# light.vera
https://github.com/balloob/home-assistant-vera-api/archive/a8f823066ead6c7da6fb5e7abaf16fef62e63364.zip#python-vera==0.1
# Sonos bindings (media_player.sonos)
SoCo==0.11.1
# PlexAPI (media_player.plex)
https://github.com/miniconfig/python-plexapi/archive/437e36dca3b7780dc0cb73941d662302c0cd2fa9.zip#python-plexapi==1.0.2

9
script/bootstrap Executable file
View File

@ -0,0 +1,9 @@
#!/bin/sh
# script/bootstrap: Resolve all dependencies that the application requires to
# run.
cd "$(dirname "$0")/.."
script/bootstrap_server
script/bootstrap_frontend

5
script/bootstrap_frontend Executable file
View File

@ -0,0 +1,5 @@
echo "Bootstrapping frontend..."
cd homeassistant/components/frontend/www_static/home-assistant-polymer
npm install
npm run setup_js_dev
cd ../../../../..

10
script/bootstrap_server Executable file
View File

@ -0,0 +1,10 @@
cd "$(dirname "$0")/.."
echo "Update the submodule to latest version..."
git submodule update
echo "Installing dependencies..."
python3 -m pip install --upgrade -r requirements_all.txt
echo "Installing development dependencies.."
python3 -m pip install --upgrade flake8 pylint coveralls pytest pytest-cov

View File

@ -1,12 +1,8 @@
# Builds the frontend for production
# If current pwd is scripts, go 1 up.
if [ ${PWD##*/} == "scripts" ]; then
cd ..
fi
cd "$(dirname "$0")/.."
cd homeassistant/components/frontend/www_static/home-assistant-polymer
npm install
npm run frontend_prod
cp bower_components/webcomponentsjs/webcomponents-lite.min.js ..

View File

@ -3,10 +3,7 @@
# apt-get install cython3 libudev-dev python-sphinx python3-setuptools
# pip3 install cython
# If current pwd is scripts, go 1 up.
if [ ${PWD##*/} == "scripts" ]; then
cd ..
fi
cd "$(dirname "$0")/.."
if [ ! -d build ]; then
mkdir build

14
script/cibuild Executable file
View File

@ -0,0 +1,14 @@
#!/bin/sh
# script/cibuild: Setup environment for CI to run tests. This is primarily
# designed to run on the continuous integration server.
cd "$(dirname "$0")/.."
script/test coverage
STATUS=$?
coveralls
exit $STATUS

View File

@ -3,10 +3,7 @@
# Optional: pass in a timezone as first argument
# If not given will attempt to mount /etc/localtime
# If current pwd is scripts, go 1 up.
if [ ${PWD##*/} == "scripts" ]; then
cd ..
fi
cd "$(dirname "$0")/.."
docker build -t home-assistant-dev .

View File

@ -1,10 +1,7 @@
# Open a docker that can be used to debug/dev python-openzwave
# Pass in a command line argument to build
# If current pwd is scripts, go 1 up.
if [ ${PWD##*/} == "scripts" ]; then
cd ..
fi
cd "$(dirname "$0")/.."
if [ $# -gt 0 ]
then

8
scripts/hass-daemon → script/hass-daemon Normal file → Executable file
View File

@ -34,25 +34,27 @@ RUN_AS="USER"
PID_FILE="/var/run/hass.pid"
CONFIG_DIR="/var/opt/homeassistant"
FLAGS="-v --config $CONFIG_DIR --pid-file $PID_FILE --daemon"
REDIRECT="> $CONFIG_DIR/home-assistant.log 2>&1"
start() {
if [ -f $PID_FILE ] && kill -0 $(cat $PID_FILE); then
if [ -f $PID_FILE ] && kill -0 $(cat $PID_FILE) 2> /dev/null; then
echo 'Service already running' >&2
return 1
fi
echo 'Starting service…' >&2
local CMD="$PRE_EXEC hass $FLAGS;"
local CMD="$PRE_EXEC hass $FLAGS $REDIRECT;"
su -c "$CMD" $RUN_AS
echo 'Service started' >&2
}
stop() {
if [ ! -f "$PID_FILE" ] || ! kill -0 $(cat "$PID_FILE"); then
if [ ! -f "$PID_FILE" ] || ! kill -0 $(cat "$PID_FILE") 2> /dev/null; then
echo 'Service not running' >&2
return 1
fi
echo 'Stopping service…' >&2
kill -3 $(cat "$PID_FILE")
while ps -p $(cat "$PID_FILE") > /dev/null 2>&1; do sleep 1;done;
echo 'Service stopped' >&2
}

View 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

19
script/lint Executable file
View File

@ -0,0 +1,19 @@
# Run style checks
cd "$(dirname "$0")/.."
echo "Checking style with flake8..."
flake8 --exclude www_static homeassistant
FLAKE8_STATUS=$?
echo "Checking style with pylint..."
pylint homeassistant
PYLINT_STATUS=$?
if [ $FLAKE8_STATUS -eq 0 ]
then
exit $PYLINT_STATUS
else
exit $FLAKE8_STATUS
fi

21
script/release Executable file
View 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

8
script/server Executable file
View File

@ -0,0 +1,8 @@
#!/bin/sh
# script/server: Launch the application and any extra required processes
# locally.
cd "$(dirname "$0")/.."
python3 -m homeassistant -c config

5
script/setup Executable file
View File

@ -0,0 +1,5 @@
cd "$(dirname "$0")/.."
git submodule init
script/bootstrap
python3 setup.py develop

27
script/test Executable file
View File

@ -0,0 +1,27 @@
#!/bin/sh
# script/test: Run test suite for application. Optionallly pass in a path to an
# individual test file to run a single test.
cd "$(dirname "$0")/.."
script/lint
LINT_STATUS=$?
echo "Running tests..."
if [ "$1" = "coverage" ]; then
py.test --cov --cov-report=
TEST_STATUS=$?
else
py.test
TEST_STATUS=$?
fi
if [ $LINT_STATUS -eq 0 ]
then
exit $TEST_STATUS
else
exit $LINT_STATUS
fi

8
script/update Executable file
View File

@ -0,0 +1,8 @@
#!/bin/sh
# script/update: Update application to run for its current checkout.
cd "$(dirname "$0")/.."
git pull
git submodule update

View File

@ -1,9 +0,0 @@
# Run style checks
# If current pwd is scripts, go 1 up.
if [ ${PWD##*/} == "scripts" ]; then
cd ..
fi
flake8 homeassistant
pylint homeassistant

View File

@ -1,10 +0,0 @@
# If current pwd is scripts, go 1 up.
if [ ${PWD##*/} == "scripts" ]; then
cd ..
fi
if [ "$1" = "coverage" ]; then
coverage run -m unittest discover tests
else
python3 -m unittest discover tests
fi

View File

@ -1,6 +0,0 @@
echo "The update script has been deprecated since Home Assistant v0.7"
echo
echo "Home Assistant is now distributed via PyPi and can be installed and"
echo "upgraded by running: pip3 install --upgrade homeassistant"
echo
echo "If you are developing a new feature for Home Assistant, run: git pull"

View File

@ -12,7 +12,8 @@ PACKAGES = find_packages(exclude=['tests', 'tests.*'])
PACKAGE_DATA = \
{'homeassistant.components.frontend': ['index.html.template'],
'homeassistant.components.frontend.www_static': ['*.*'],
'homeassistant.components.frontend.www_static.images': ['*.*']}
'homeassistant.components.frontend.www_static.images': ['*.*'],
'homeassistant.startup': ['*.*']}
REQUIRES = [
'requests>=2,<3',

View File

@ -10,11 +10,11 @@ from unittest import mock
from homeassistant import core as ha, loader
import homeassistant.util.location as location_util
import homeassistant.util.dt as dt_util
from homeassistant.helpers.entity import ToggleEntity
from homeassistant.const import (
STATE_ON, STATE_OFF, DEVICE_DEFAULT_NAME, EVENT_TIME_CHANGED,
EVENT_STATE_CHANGED)
EVENT_STATE_CHANGED, EVENT_PLATFORM_DISCOVERED, ATTR_SERVICE,
ATTR_DISCOVERED)
from homeassistant.components import sun, mqtt
@ -38,8 +38,8 @@ def get_test_home_assistant(num_threads=None):
hass.config.latitude = 32.87336
hass.config.longitude = -117.22743
# if not loader.PREPARED:
loader. prepare(hass)
if 'custom_components.test' not in loader.AVAILABLE_COMPONENTS:
loader.prepare(hass)
return hass
@ -86,10 +86,11 @@ def fire_time_changed(hass, time):
hass.bus.fire(EVENT_TIME_CHANGED, {'now': time})
def trigger_device_tracker_scan(hass):
""" Triggers the device tracker to scan. """
fire_time_changed(
hass, dt_util.utcnow().replace(second=0) + timedelta(hours=1))
def fire_service_discovered(hass, service, info):
hass.bus.fire(EVENT_PLATFORM_DISCOVERED, {
ATTR_SERVICE: service,
ATTR_DISCOVERED: info
})
def ensure_sun_risen(hass):

View File

@ -1,15 +1,13 @@
"""
tests.test_component_demo
~~~~~~~~~~~~~~~~~~~~~~~~~~~
tests.components.automation.test_event
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Tests demo component.
Tests event automation.
"""
import unittest
import homeassistant.core as ha
import homeassistant.components.automation as automation
import homeassistant.components.automation.event as event
from homeassistant.const import CONF_PLATFORM
class TestAutomationEvent(unittest.TestCase):
@ -28,20 +26,57 @@ class TestAutomationEvent(unittest.TestCase):
""" Stop down stuff we started. """
self.hass.stop()
def test_fails_setup_if_no_event_type(self):
self.assertFalse(automation.setup(self.hass, {
def test_old_config_if_fires_on_event(self):
self.assertTrue(automation.setup(self.hass, {
automation.DOMAIN: {
CONF_PLATFORM: 'event',
automation.CONF_SERVICE: 'test.automation'
'platform': 'event',
'event_type': 'test_event',
'execute_service': 'test.automation'
}
}))
self.hass.bus.fire('test_event')
self.hass.pool.block_till_done()
self.assertEqual(1, len(self.calls))
def test_old_config_if_fires_on_event_with_data(self):
self.assertTrue(automation.setup(self.hass, {
automation.DOMAIN: {
'platform': 'event',
'event_type': 'test_event',
'event_data': {'some_attr': 'some_value'},
'execute_service': 'test.automation'
}
}))
self.hass.bus.fire('test_event', {'some_attr': 'some_value'})
self.hass.pool.block_till_done()
self.assertEqual(1, len(self.calls))
def test_old_config_if_not_fires_if_event_data_not_matches(self):
self.assertTrue(automation.setup(self.hass, {
automation.DOMAIN: {
'platform': 'event',
'event_type': 'test_event',
'event_data': {'some_attr': 'some_value'},
'execute_service': 'test.automation'
}
}))
self.hass.bus.fire('test_event', {'some_attr': 'some_other_value'})
self.hass.pool.block_till_done()
self.assertEqual(0, len(self.calls))
def test_if_fires_on_event(self):
self.assertTrue(automation.setup(self.hass, {
automation.DOMAIN: {
CONF_PLATFORM: 'event',
event.CONF_EVENT_TYPE: 'test_event',
automation.CONF_SERVICE: 'test.automation'
'trigger': {
'platform': 'event',
'event_type': 'test_event',
},
'action': {
'service': 'test.automation',
}
}
}))
@ -52,24 +87,33 @@ class TestAutomationEvent(unittest.TestCase):
def test_if_fires_on_event_with_data(self):
self.assertTrue(automation.setup(self.hass, {
automation.DOMAIN: {
CONF_PLATFORM: 'event',
event.CONF_EVENT_TYPE: 'test_event',
event.CONF_EVENT_DATA: {'some_attr': 'some_value'},
automation.CONF_SERVICE: 'test.automation'
'trigger': {
'platform': 'event',
'event_type': 'test_event',
'event_data': {'some_attr': 'some_value'}
},
'action': {
'service': 'test.automation',
}
}
}))
self.hass.bus.fire('test_event', {'some_attr': 'some_value'})
self.hass.bus.fire('test_event', {'some_attr': 'some_value',
'another': 'value'})
self.hass.pool.block_till_done()
self.assertEqual(1, len(self.calls))
def test_if_not_fires_if_event_data_not_matches(self):
self.assertTrue(automation.setup(self.hass, {
automation.DOMAIN: {
CONF_PLATFORM: 'event',
event.CONF_EVENT_TYPE: 'test_event',
event.CONF_EVENT_DATA: {'some_attr': 'some_value'},
automation.CONF_SERVICE: 'test.automation'
'trigger': {
'platform': 'event',
'event_type': 'test_event',
'event_data': {'some_attr': 'some_value'}
},
'action': {
'service': 'test.automation',
}
}
}))

Some files were not shown because too many files have changed in this diff Show More