Merge remote-tracking branch 'upstream/master' into scheduler

* upstream/master: (104 commits)
  Fire a time_changed event every second
  Update example config with correct wink config
  Wink API is weird.  If you delete a device from their API, they dont delete it.  They just "hide" it
  Update the frontend with the new icon for sensor
  Minor refactor of build_frontend script to support linux, and not just mac
  Update script installs latest dependencies
  Fix flaky device scanner test
  Increased environment validation upon start
  Fix group names for switch, light and devices
  Disable pylint unused-argument check
  Fix device scanner test
  Better update schedules for cast and devicetracker
  Tweaks to the configurator UI
  Add tests, fix styling
  Add initial version of configurator component
  Fix tabs being selectable by clicking on header
  Data binding fix: Update instead of replace states
  New: State.last_updated represents creation date
  Update sensor icon for now
  Updates to resolve flake8 errors
  ...
This commit is contained in:
Gustav Ahlberg 2015-01-25 21:44:54 +01:00
commit 631251f1f7
119 changed files with 4351 additions and 1452 deletions

6
.gitmodules vendored
View File

@ -4,3 +4,9 @@
[submodule "homeassistant/external/pywemo"]
path = homeassistant/external/pywemo
url = https://github.com/balloob/pywemo.git
[submodule "homeassistant/external/netdisco"]
path = homeassistant/external/netdisco
url = https://github.com/balloob/netdisco.git
[submodule "homeassistant/external/noop"]
path = homeassistant/external/noop
url = https://github.com/balloob/noop.git

View File

@ -7,6 +7,6 @@ install:
script:
- flake8 homeassistant --exclude bower_components,external
- pylint homeassistant
- coverage run --source=homeassistant -m unittest discover ha_test
- coverage run --source=homeassistant --omit "homeassistant/external/*" -m unittest discover tests
after_success:
- coveralls

View File

@ -3,6 +3,4 @@ MAINTAINER Paulus Schoutsen <Paulus@PaulusSchoutsen.nl>
VOLUME /config
EXPOSE 8123
CMD [ "python", "-m", "homeassistant", "--config", "/config" ]

View File

@ -33,26 +33,27 @@ If you run into issues while using Home Assistant or during development of a com
## Installation instructions / Quick-start guide
Running Home Assistant requires that python3 and the package requests are installed.
Run the following code to get up and running with the minimum setup:
Running Home Assistant requires that python3 and the package requests are installed. Run the following code to install and start Home Assistant:
```python
git clone --recursive https://github.com/balloob/home-assistant.git
cd home-assistant
pip3 install -r requirements.txt
python3 -m homeassistant
python3 -m homeassistant --open-ui
```
This will start the Home Assistant server and create an initial configuration file in `config/home-assistant.conf` that is setup for demo mode. It will launch its web interface on [http://127.0.0.1:8123](http://127.0.0.1:8123). The default password is 'password'.
The last command will start the Home Assistant server and launch its webinterface. By default Home Assistant looks for the configuration file `config/home-assistant.conf`. A standard configuration file will be written if none exists.
If you are still exploring if you want to use Home Assistant in the first place, you can enable the demo mode by adding the `--demo-mode` argument to the last command.
If you're using Docker, you can use
```bash
docker run -d --name="home-assistant" -v /path/to/homeassistant/config:/config -v /etc/localtime:/etc/localtime:ro -p 8123:8123 balloob/home-assistant
docker run -d --name="home-assistant" -v /path/to/homeassistant/config:/config -v /etc/localtime:/etc/localtime:ro --net=host balloob/home-assistant
```
After you have launched the Docker image, navigate to its web interface on [http://127.0.0.1:8123](http://127.0.0.1:8123).
After you got the demo mode running it is time to enable some real components and get started. An example configuration file has been provided in [/config/home-assistant.conf.example](https://github.com/balloob/home-assistant/blob/master/config/home-assistant.conf.example).
*Note:* you can append `?api_password=YOUR_PASSWORD` to the url of the web interface to log in automatically.

View File

@ -34,7 +34,6 @@ SERVICE_FLASH = 'flash'
_LOGGER = logging.getLogger(__name__)
# pylint: disable=unused-argument
def setup(hass, config):
""" Setup example component. """

View File

@ -11,6 +11,10 @@ api_password=mypass
[light]
platform=hue
[wink]
# Get your token at https://winkbearertoken.appspot.com
access_token=YOUR_TOKEN
[device_tracker]
# The following types are available: netgear, tomato, luci, nmap_tracker
platform=netgear
@ -33,9 +37,19 @@ platform=wemo
# Optional: hard code the hosts (comma seperated) to avoid scanning the network
# hosts=192.168.1.9,192.168.1.12
[thermostat]
platform=nest
# Required: username and password that are used to login to the Nest thermostat.
username=myemail@mydomain.com
password=mypassword
[downloader]
download_dir=downloads
[notify]
platform=pushbullet
api_key=ABCDEFGHJKLMNOPQRSTUVXYZ
[device_sun_light_trigger]
# Optional: specify a specific light/group of lights that has to be turned on
light_group=group.living_room
@ -65,3 +79,26 @@ unknown_light=group.living_room
[browser]
[keyboard]
[automation]
platform=state
alias=Sun starts shining
state_entity_id=sun.sun
# Next two are optional, omit to match all
state_from=below_horizon
state_to=above_horizon
execute_service=light.turn_off
service_entity_id=group.living_room
[automation 2]
platform=time
alias=Beer o Clock
time_hours=16
time_minutes=0
time_seconds=0
execute_service=notify.notify
execute_service_data={"message":"It's 4, time for beer!"}

View File

@ -27,7 +27,7 @@ import homeassistant.util as util
DOMAIN = "homeassistant"
# How often time_changed event should fire
TIMER_INTERVAL = 10 # seconds
TIMER_INTERVAL = 1 # seconds
# How long we wait for the result of a service call
SERVICE_CALL_LIMIT = 10 # seconds
@ -52,6 +52,13 @@ class HomeAssistant(object):
self.services = ServiceRegistry(self.bus, pool)
self.states = StateMachine(self.bus)
# List of loaded components
self.components = []
# Remote.API object pointing at local API
self.local_api = None
# Directory that holds the configuration
self.config_dir = os.path.join(os.getcwd(), 'config')
def get_config_path(self, path):
@ -219,10 +226,10 @@ def _process_match_param(parameter):
""" Wraps parameter in a list if it is not one and returns it. """
if parameter is None or parameter == MATCH_ALL:
return MATCH_ALL
elif isinstance(parameter, list):
return parameter
elif isinstance(parameter, str) or not hasattr(parameter, '__iter__'):
return (parameter,)
else:
return [parameter]
return tuple(parameter)
def _matcher(subject, pattern):
@ -412,26 +419,36 @@ class EventBus(object):
class State(object):
""" Object to represent a state within the state machine. """
"""
Object to represent a state within the state machine.
__slots__ = ['entity_id', 'state', 'attributes', 'last_changed']
entity_id: the entity that is represented.
state: the state of the entity
attributes: extra information on entity and state
last_changed: last time the state was changed, not the attributes.
last_updated: last time this object was updated.
"""
__slots__ = ['entity_id', 'state', 'attributes',
'last_changed', 'last_updated']
def __init__(self, entity_id, state, attributes=None, last_changed=None):
if not ENTITY_ID_PATTERN.match(entity_id):
raise InvalidEntityFormatError((
"Invalid entity id encountered: {}. "
"Format should be <domain>.<entity>").format(entity_id))
"Format should be <domain>.<object_id>").format(entity_id))
self.entity_id = entity_id
self.state = state
self.attributes = attributes or {}
self.last_updated = dt.datetime.now()
# Strip microsecond from last_changed else we cannot guarantee
# state == State.from_dict(state.as_dict())
# This behavior occurs because to_dict uses datetime_to_str
# which does not preserve microseconds
self.last_changed = util.strip_microseconds(
last_changed or dt.datetime.now())
last_changed or self.last_updated)
def copy(self):
""" Creates a copy of itself. """
@ -467,17 +484,17 @@ class State(object):
def __eq__(self, other):
return (self.__class__ == other.__class__ and
self.entity_id == other.entity_id and
self.state == other.state and
self.attributes == other.attributes)
def __repr__(self):
if self.attributes:
return "<state {}:{} @ {}>".format(
self.state, util.repr_helper(self.attributes),
util.datetime_to_str(self.last_changed))
else:
return "<state {} @ {}>".format(
self.state, util.datetime_to_str(self.last_changed))
attr = "; {}".format(util.repr_helper(self.attributes)) \
if self.attributes else ""
return "<state {}={}{} @ {}>".format(
self.entity_id, self.state, attr,
util.datetime_to_str(self.last_changed))
class StateMachine(object):
@ -513,15 +530,12 @@ class StateMachine(object):
def get_since(self, point_in_time):
"""
Returns all states that have been changed since point_in_time.
Note: States keep track of last_changed -without- microseconds.
Therefore your point_in_time will also be stripped of microseconds.
"""
point_in_time = util.strip_microseconds(point_in_time)
with self._lock:
return [state for state in self._states.values()
if state.last_changed >= point_in_time]
if state.last_updated >= point_in_time]
def is_state(self, entity_id, state):
""" Returns True if entity exists and is specified state. """
@ -538,20 +552,28 @@ class StateMachine(object):
def set(self, entity_id, new_state, attributes=None):
""" Set the state of an entity, add entity if it does not exist.
Attributes is an optional dict to specify attributes of this state. """
Attributes is an optional dict to specify attributes of this state.
If you just update the attributes and not the state, last changed will
not be affected.
"""
new_state = str(new_state)
attributes = attributes or {}
with self._lock:
old_state = self._states.get(entity_id)
is_existing = old_state is not None
same_state = is_existing and old_state.state == new_state
same_attr = is_existing and old_state.attributes == attributes
# If state did not exist or is different, set it
if not old_state or \
old_state.state != new_state or \
old_state.attributes != attributes:
if not (same_state and same_attr):
last_changed = old_state.last_changed if same_state else None
state = self._states[entity_id] = \
State(entity_id, new_state, attributes)
State(entity_id, new_state, attributes, last_changed)
event_data = {'entity_id': entity_id, 'new_state': state}
@ -574,9 +596,9 @@ class StateMachine(object):
# Ensure it is a lowercase list with entity ids we want to match on
if isinstance(entity_ids, str):
entity_ids = [entity_ids.lower()]
entity_ids = (entity_ids.lower(),)
else:
entity_ids = [entity_id.lower() for entity_id in entity_ids]
entity_ids = tuple(entity_id.lower() for entity_id in entity_ids)
@ft.wraps(action)
def state_listener(event):

View File

@ -1,36 +1,23 @@
""" Starts home assistant. """
from __future__ import print_function
import sys
import os
import argparse
import importlib
try:
from homeassistant import bootstrap
except ImportError:
# This is to add support to load Home Assistant using
# `python3 homeassistant` instead of `python3 -m homeassistant`
def validate_python():
""" Validate we're running the right Python version. """
major, minor = sys.version_info[:2]
# Insert the parent directory of this file into the module search path
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
from homeassistant import bootstrap
if major < 3 or (major == 3 and minor < 4):
print("Home Assistant requires atleast Python 3.4")
sys.exit()
def main():
""" Starts Home Assistant. Will create demo config if no config found. """
parser = argparse.ArgumentParser()
parser.add_argument(
'-c', '--config',
metavar='path_to_config_dir',
default="config",
help="Directory that contains the Home Assistant configuration")
args = parser.parse_args()
# Validate that all core dependencies are installed
def validate_dependencies():
""" Validate all dependencies that HA uses. """
import_fail = False
for module in ['requests']:
@ -44,11 +31,42 @@ def main():
if import_fail:
print(("Install dependencies by running: "
"pip3 install -r requirements.txt"))
exit()
sys.exit()
def ensure_path_and_load_bootstrap():
""" Ensure sys load path is correct and load Home Assistant bootstrap. """
try:
from homeassistant import bootstrap
except ImportError:
# This is to add support to load Home Assistant using
# `python3 homeassistant` instead of `python3 -m homeassistant`
# Insert the parent directory of this file into the module search path
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
from homeassistant import bootstrap
return bootstrap
def validate_git_submodules():
""" Validate the git submodules are cloned. """
try:
# pylint: disable=no-name-in-module, unused-variable
from homeassistant.external.noop import WORKING # noqa
except ImportError:
print("Repository submodules have not been initialized")
print("Please run: git submodule update --init --recursive")
sys.exit()
def ensure_config_path(config_dir):
""" Gets the path to the configuration file.
Creates one if it not exists. """
# Test if configuration directory exists
config_dir = os.path.join(os.getcwd(), args.config)
if not os.path.isdir(config_dir):
print(('Fatal Error: Unable to find specified configuration '
'directory {} ').format(config_dir))
@ -60,15 +78,72 @@ def main():
if not os.path.isfile(config_path):
try:
with open(config_path, 'w') as conf:
conf.write("[http]\n")
conf.write("api_password=password\n\n")
conf.write("[demo]\n")
conf.write("[http]\n\n")
conf.write("[discovery]\n\n")
except IOError:
print(('Fatal Error: No configuration file found and unable '
'to write a default one to {}').format(config_path))
sys.exit()
hass = bootstrap.from_config_file(config_path)
return config_path
def get_arguments():
""" Get parsed passed in arguments. """
parser = argparse.ArgumentParser()
parser.add_argument(
'-c', '--config',
metavar='path_to_config_dir',
default="config",
help="Directory that contains the Home Assistant configuration")
parser.add_argument(
'--demo-mode',
action='store_true',
help='Start Home Assistant in demo mode')
parser.add_argument(
'--open-ui',
action='store_true',
help='Open the webinterface in a browser')
return parser.parse_args()
def main():
""" Starts Home Assistant. """
validate_python()
validate_dependencies()
bootstrap = ensure_path_and_load_bootstrap()
validate_git_submodules()
args = get_arguments()
config_dir = os.path.join(os.getcwd(), args.config)
config_path = ensure_config_path(config_dir)
if args.demo_mode:
from homeassistant.components import http, demo
# Demo mode only requires http and demo components.
hass = bootstrap.from_config_dict({
http.DOMAIN: {},
demo.DOMAIN: {}
})
else:
hass = bootstrap.from_config_file(config_path)
if args.open_ui:
from homeassistant.const import EVENT_HOMEASSISTANT_START
def open_browser(event):
""" Open the webinterface in a browser. """
if hass.local_api is not None:
import webbrowser
webbrowser.open(hass.local_api.base_url)
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, open_browser)
hass.start()
hass.block_till_stopped()

View File

@ -17,6 +17,38 @@ from collections import defaultdict
import homeassistant
import homeassistant.loader as loader
import homeassistant.components as core_components
import homeassistant.components.group as group
_LOGGER = logging.getLogger(__name__)
def setup_component(hass, domain, config=None):
""" Setup a component for Home Assistant. """
if config is None:
config = defaultdict(dict)
component = loader.get_component(domain)
try:
if component.setup(hass, config):
hass.components.append(component.DOMAIN)
_LOGGER.info("component %s initialized", domain)
# Assumption: if a component does not depend on groups
# it communicates with devices
if group.DOMAIN not in component.DEPENDENCIES:
hass.pool.add_worker()
return True
else:
_LOGGER.error("component %s failed to initialize", domain)
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Error during setup of component %s", domain)
return False
# pylint: disable=too-many-branches, too-many-statements
@ -29,7 +61,7 @@ def from_config_dict(config, hass=None):
if hass is None:
hass = homeassistant.HomeAssistant()
logger = logging.getLogger(__name__)
enable_logging(hass)
loader.prepare(hass)
@ -42,42 +74,21 @@ def from_config_dict(config, hass=None):
if ' ' not in key and key != homeassistant.DOMAIN)
if not core_components.setup(hass, config):
logger.error(("Home Assistant core failed to initialize. "
"Further initialization aborted."))
_LOGGER.error("Home Assistant core failed to initialize. "
"Further initialization aborted.")
return hass
logger.info("Home Assistant core initialized")
_LOGGER.info("Home Assistant core initialized")
# Setup the components
# We assume that all components that load before the group component loads
# are components that poll devices. As their tasks are IO based, we will
# add an extra worker for each of them.
add_worker = True
for domain in loader.load_order_components(components):
component = loader.get_component(domain)
try:
if component.setup(hass, config):
logger.info("component %s initialized", domain)
add_worker = add_worker and domain != "group"
if add_worker:
hass.pool.add_worker()
else:
logger.error("component %s failed to initialize", domain)
except Exception: # pylint: disable=broad-except
logger.exception("Error during setup of component %s", domain)
setup_component(hass, domain, config)
return hass
def from_config_file(config_path, hass=None, enable_logging=True):
def from_config_file(config_path, hass=None):
"""
Reads the configuration file and tries to start all the required
functionality. Will add functionality to 'hass' parameter if given,
@ -89,32 +100,6 @@ def from_config_file(config_path, hass=None, enable_logging=True):
# Set config dir to directory holding config file
hass.config_dir = os.path.abspath(os.path.dirname(config_path))
if enable_logging:
# Setup the logging for home assistant.
logging.basicConfig(level=logging.INFO)
# Log errors to a file if we have write access to file or config dir
err_log_path = hass.get_config_path("home-assistant.log")
err_path_exists = os.path.isfile(err_log_path)
# Check if we can write to the error log if it exists or that
# we can create files in the containgin directory if not.
if (err_path_exists and os.access(err_log_path, os.W_OK)) or \
(not err_path_exists and os.access(hass.config_dir, os.W_OK)):
err_handler = logging.FileHandler(
err_log_path, mode='w', delay=True)
err_handler.setLevel(logging.WARNING)
err_handler.setFormatter(
logging.Formatter('%(asctime)s %(name)s: %(message)s',
datefmt='%H:%M %d-%m-%y'))
logging.getLogger('').addHandler(err_handler)
else:
logging.getLogger(__name__).error(
"Unable to setup error log %s (access denied)", err_log_path)
# Read config
config = configparser.ConfigParser()
config.read(config_path)
@ -128,3 +113,30 @@ def from_config_file(config_path, hass=None, enable_logging=True):
config_dict[section][key] = val
return from_config_dict(config_dict, hass)
def enable_logging(hass):
""" Setup the logging for home assistant. """
logging.basicConfig(level=logging.INFO)
# Log errors to a file if we have write access to file or config dir
err_log_path = hass.get_config_path("home-assistant.log")
err_path_exists = os.path.isfile(err_log_path)
# Check if we can write to the error log if it exists or that
# we can create files in the containing directory if not.
if (err_path_exists and os.access(err_log_path, os.W_OK)) or \
(not err_path_exists and os.access(hass.config_dir, os.W_OK)):
err_handler = logging.FileHandler(
err_log_path, mode='w', delay=True)
err_handler.setLevel(logging.WARNING)
err_handler.setFormatter(
logging.Formatter('%(asctime)s %(name)s: %(message)s',
datefmt='%H:%M %d-%m-%y'))
logging.getLogger('').addHandler(err_handler)
else:
_LOGGER.error(
"Unable to setup error log %s (access denied)", err_log_path)

View File

@ -70,7 +70,6 @@ def turn_off(hass, entity_id=None, **service_data):
hass.services.call(ha.DOMAIN, SERVICE_TURN_OFF, service_data)
# pylint: disable=unused-argument
def setup(hass, config):
""" Setup general services related to homeassistant. """

View File

@ -0,0 +1,71 @@
"""
homeassistant.components.automation
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Allows to setup simple automation rules via the config file.
"""
import logging
import json
from homeassistant.loader import get_component
from homeassistant.helpers import config_per_platform
from homeassistant.util import convert, split_entity_id
from homeassistant.const import ATTR_ENTITY_ID
DOMAIN = "automation"
DEPENDENCIES = ["group"]
CONF_ALIAS = "alias"
CONF_SERVICE = "execute_service"
CONF_SERVICE_ENTITY_ID = "service_entity_id"
CONF_SERVICE_DATA = "service_data"
_LOGGER = logging.getLogger(__name__)
def setup(hass, config):
""" Sets up automation. """
for p_type, p_config in config_per_platform(config, DOMAIN, _LOGGER):
platform = get_component('automation.{}'.format(p_type))
if platform is None:
_LOGGER.error("Unknown automation platform specified: %s", p_type)
continue
if platform.register(hass, p_config, _get_action(hass, p_config)):
_LOGGER.info(
"Initialized %s rule %s", p_type, p_config.get(CONF_ALIAS, ""))
else:
_LOGGER.error(
"Error setting up rule %s", p_config.get(CONF_ALIAS, ""))
return True
def _get_action(hass, config):
""" Return an action based on a config. """
def action():
""" Action to be executed. """
_LOGGER.info("Executing rule %s", config.get(CONF_ALIAS, ""))
if CONF_SERVICE in config:
domain, service = split_entity_id(config[CONF_SERVICE])
service_data = convert(
config.get(CONF_SERVICE_DATA), json.loads, {})
if not isinstance(service_data, dict):
_LOGGER.error(
"%s should be a serialized JSON object", CONF_SERVICE_DATA)
service_data = {}
if CONF_SERVICE_ENTITY_ID in config:
service_data[ATTR_ENTITY_ID] = \
config[CONF_SERVICE_ENTITY_ID].split(",")
hass.services.call(domain, service, service_data)
return action

View File

@ -0,0 +1,36 @@
"""
homeassistant.components.automation.state
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Offers state listening automation rules.
"""
import logging
from homeassistant.const import MATCH_ALL
CONF_ENTITY_ID = "state_entity_id"
CONF_FROM = "state_from"
CONF_TO = "state_to"
def register(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)
return False
from_state = config.get(CONF_FROM, MATCH_ALL)
to_state = config.get(CONF_TO, MATCH_ALL)
def state_automation_listener(entity, from_s, to_s):
""" Listens for state changes and calls action. """
action()
hass.states.track_change(
entity_id, state_automation_listener, from_state, to_state)
return True

View File

@ -0,0 +1,28 @@
"""
homeassistant.components.automation.time
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Offers time listening automation rules.
"""
from homeassistant.util import convert
CONF_HOURS = "time_hours"
CONF_MINUTES = "time_minutes"
CONF_SECONDS = "time_seconds"
def register(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)
def time_automation_listener(now):
""" Listens for time changes and calls action. """
action()
hass.track_time_change(
time_automation_listener,
hour=hours, minute=minutes, second=seconds)
return True

View File

@ -11,7 +11,6 @@ DEPENDENCIES = []
SERVICE_BROWSE_URL = "browse_url"
# pylint: disable=unused-argument
def setup(hass, config):
""" Listen for browse_url events and open
the url in the default webbrowser. """

View File

@ -6,13 +6,19 @@ Provides functionality to interact with Chromecasts.
"""
import logging
try:
import pychromecast
except ImportError:
# Ignore, we will raise appropriate error later
pass
from homeassistant.loader import get_component
import homeassistant.util as util
from homeassistant.helpers import extract_entity_ids
from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, SERVICE_TURN_OFF, SERVICE_VOLUME_UP,
SERVICE_VOLUME_DOWN, SERVICE_MEDIA_PLAY_PAUSE, SERVICE_MEDIA_PLAY,
SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PREV_TRACK,
CONF_HOSTS)
SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PREV_TRACK)
DOMAIN = 'chromecast'
@ -105,12 +111,35 @@ def media_prev_track(hass, entity_id=None):
hass.services.call(DOMAIN, SERVICE_MEDIA_PREV_TRACK, data)
# pylint: disable=too-many-locals, too-many-branches
def setup(hass, config):
""" Listen for chromecast events. """
logger = logging.getLogger(__name__)
def setup_chromecast(casts, host):
""" Tries to convert host to Chromecast object and set it up. """
# Check if already setup
if any(cast.host == host for cast in casts.values()):
return
try:
cast = pychromecast.PyChromecast(host)
entity_id = util.ensure_unique_string(
ENTITY_ID_FORMAT.format(
util.slugify(cast.device.friendly_name)),
casts.keys())
casts[entity_id] = cast
except pychromecast.ChromecastConnectionError:
pass
def setup(hass, config):
# pylint: disable=unused-argument,too-many-locals
""" Listen for chromecast events. """
logger = logging.getLogger(__name__)
discovery = get_component('discovery')
try:
# pylint: disable=redefined-outer-name
import pychromecast
except ImportError:
logger.exception(("Failed to import pychromecast. "
@ -119,33 +148,23 @@ def setup(hass, config):
return False
if CONF_HOSTS in config[DOMAIN]:
hosts = config[DOMAIN][CONF_HOSTS].split(",")
casts = {}
# If no hosts given, scan for chromecasts
else:
# If discovery component not loaded, scan ourselves
if discovery.DOMAIN not in hass.components:
logger.info("Scanning for Chromecasts")
hosts = pychromecast.discover_chromecasts()
casts = {}
for host in hosts:
setup_chromecast(casts, host)
for host in hosts:
try:
cast = pychromecast.PyChromecast(host)
def chromecast_discovered(service, info):
""" Called when a Chromecast has been discovered. """
logger.info("New Chromecast discovered: %s", info[0])
setup_chromecast(casts, info[0])
entity_id = util.ensure_unique_string(
ENTITY_ID_FORMAT.format(
util.slugify(cast.device.friendly_name)),
casts.keys())
casts[entity_id] = cast
except pychromecast.ChromecastConnectionError:
pass
if not casts:
logger.error("Could not find Chromecasts")
return False
discovery.listen(
hass, discovery.services.GOOGLE_CAST, chromecast_discovered)
def update_chromecast_state(entity_id, chromecast):
""" Retrieve state of Chromecast and update statemachine. """
@ -192,12 +211,13 @@ def setup(hass, config):
hass.states.set(entity_id, state, state_attr)
def update_chromecast_states(time): # pylint: disable=unused-argument
def update_chromecast_states(time):
""" Updates all chromecast states. """
logger.info("Updating Chromecast status")
if casts:
logger.info("Updating Chromecast status")
for entity_id, cast in casts.items():
update_chromecast_state(entity_id, cast)
for entity_id, cast in casts.items():
update_chromecast_state(entity_id, cast)
def _service_to_entities(service):
""" Helper method to get entities from service. """
@ -277,7 +297,7 @@ def setup(hass, config):
pychromecast.play_youtube_video(video_id, cast.host)
update_chromecast_state(entity_id, cast)
hass.track_time_change(update_chromecast_states)
hass.track_time_change(update_chromecast_states, second=range(0, 60, 15))
hass.services.register(DOMAIN, SERVICE_TURN_OFF,
turn_off_service)

View File

@ -0,0 +1,190 @@
"""
homeassistant.components.configurator
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
A component to allow pieces of code to request configuration from the user.
Initiate a request by calling the `request_config` method with a callback.
This will return a request id that has to be used for future calls.
A callback has to be provided to `request_config` which will be called when
the user has submitted configuration information.
"""
import logging
from homeassistant.helpers import generate_entity_id
from homeassistant.const import EVENT_TIME_CHANGED
DOMAIN = "configurator"
DEPENDENCIES = []
ENTITY_ID_FORMAT = DOMAIN + ".{}"
SERVICE_CONFIGURE = "configure"
STATE_CONFIGURE = "configure"
STATE_CONFIGURED = "configured"
ATTR_CONFIGURE_ID = "configure_id"
ATTR_DESCRIPTION = "description"
ATTR_DESCRIPTION_IMAGE = "description_image"
ATTR_SUBMIT_CAPTION = "submit_caption"
ATTR_FIELDS = "fields"
ATTR_ERRORS = "errors"
_REQUESTS = {}
_INSTANCES = {}
_LOGGER = logging.getLogger(__name__)
# pylint: disable=too-many-arguments
def request_config(
hass, name, callback, description=None, description_image=None,
submit_caption=None, fields=None):
""" Create a new request for config.
Will return an ID to be used for sequent calls. """
instance = _get_instance(hass)
request_id = instance.request_config(
name, callback,
description, description_image, submit_caption, fields)
_REQUESTS[request_id] = instance
return request_id
def notify_errors(request_id, error):
""" Add errors to a config request. """
try:
_REQUESTS[request_id].notify_errors(request_id, error)
except KeyError:
# If request_id does not exist
pass
def request_done(request_id):
""" Mark a config request as done. """
try:
_REQUESTS.pop(request_id).request_done(request_id)
except KeyError:
# If request_id does not exist
pass
def setup(hass, config):
""" Set up Configurator. """
return True
def _get_instance(hass):
""" Get an instance per hass object. """
try:
return _INSTANCES[hass]
except KeyError:
_INSTANCES[hass] = Configurator(hass)
if DOMAIN not in hass.components:
hass.components.append(DOMAIN)
return _INSTANCES[hass]
class Configurator(object):
"""
Class to keep track of current configuration requests.
"""
def __init__(self, hass):
self.hass = hass
self._cur_id = 0
self._requests = {}
hass.services.register(
DOMAIN, SERVICE_CONFIGURE, self.handle_service_call)
# pylint: disable=too-many-arguments
def request_config(
self, name, callback,
description, description_image, submit_caption, fields):
""" Setup a request for configuration. """
entity_id = generate_entity_id(ENTITY_ID_FORMAT, name, hass=self.hass)
if fields is None:
fields = []
request_id = self._generate_unique_id()
self._requests[request_id] = (entity_id, fields, callback)
data = {
ATTR_CONFIGURE_ID: request_id,
ATTR_FIELDS: fields,
}
data.update({
key: value for key, value in [
(ATTR_DESCRIPTION, description),
(ATTR_DESCRIPTION_IMAGE, description_image),
(ATTR_SUBMIT_CAPTION, submit_caption),
] if value is not None
})
self.hass.states.set(entity_id, STATE_CONFIGURE, data)
return request_id
def notify_errors(self, request_id, error):
""" Update the state with errors. """
if not self._validate_request_id(request_id):
return
entity_id = self._requests[request_id][0]
state = self.hass.states.get(entity_id)
new_data = state.attributes
new_data[ATTR_ERRORS] = error
self.hass.states.set(entity_id, STATE_CONFIGURE, new_data)
def request_done(self, request_id):
""" Remove the config request. """
if not self._validate_request_id(request_id):
return
entity_id = self._requests.pop(request_id)[0]
# If we remove the state right away, it will not be included with
# the result fo the service call (current design limitation).
# Instead, we will set it to configured to give as feedback but delete
# it shortly after so that it is deleted when the client updates.
self.hass.states.set(entity_id, STATE_CONFIGURED)
def deferred_remove(event):
""" Remove the request state. """
self.hass.states.remove(entity_id)
self.hass.bus.listen_once(EVENT_TIME_CHANGED, deferred_remove)
def handle_service_call(self, call):
""" Handle a configure service call. """
request_id = call.data.get(ATTR_CONFIGURE_ID)
if not self._validate_request_id(request_id):
return
# pylint: disable=unused-variable
entity_id, fields, callback = self._requests[request_id]
# field validation goes here?
callback(call.data.get(ATTR_FIELDS, {}))
def _generate_unique_id(self):
""" Generates a unique configurator id. """
self._cur_id += 1
return "{}-{}".format(id(self), self._cur_id)
def _validate_request_id(self, request_id):
""" Validate that the request belongs to this instance. """
return request_id in self._requests

View File

@ -5,16 +5,21 @@ homeassistant.components.demo
Sets up a demo environment that mimics interaction with devices
"""
import random
import time
import homeassistant as ha
import homeassistant.loader as loader
from homeassistant.helpers import extract_entity_ids
from homeassistant.const import (
SERVICE_TURN_ON, SERVICE_TURN_OFF, STATE_ON, STATE_OFF,
ATTR_ENTITY_PICTURE, ATTR_ENTITY_ID, CONF_LATITUDE, CONF_LONGITUDE)
SERVICE_TURN_ON, SERVICE_TURN_OFF,
STATE_ON, STATE_OFF, TEMP_CELCIUS,
ATTR_ENTITY_PICTURE, ATTR_ENTITY_ID, ATTR_UNIT_OF_MEASUREMENT,
CONF_LATITUDE, CONF_LONGITUDE)
from homeassistant.components.light import (
ATTR_XY_COLOR, ATTR_BRIGHTNESS, GROUP_NAME_ALL_LIGHTS)
from homeassistant.util import split_entity_id
ATTR_XY_COLOR, ATTR_RGB_COLOR, ATTR_BRIGHTNESS, GROUP_NAME_ALL_LIGHTS)
from homeassistant.components.thermostat import (
ATTR_CURRENT_TEMPERATURE, ATTR_AWAY_MODE)
from homeassistant.util import split_entity_id, color_RGB_to_xy
DOMAIN = "demo"
@ -24,6 +29,7 @@ DEPENDENCIES = []
def setup(hass, config):
""" Setup a demo environment. """
group = loader.get_component('group')
configurator = loader.get_component('configurator')
config.setdefault(ha.DOMAIN, {})
config.setdefault(DOMAIN, {})
@ -48,8 +54,25 @@ def setup(hass, config):
domain, _ = split_entity_id(entity_id)
if domain == "light":
data = {ATTR_BRIGHTNESS: 200,
ATTR_XY_COLOR: random.choice(light_colors)}
rgb_color = service.data.get(ATTR_RGB_COLOR)
if rgb_color:
color = color_RGB_to_xy(
rgb_color[0], rgb_color[1], rgb_color[2])
else:
cur_state = hass.states.get(entity_id)
# Use current color if available
if cur_state and cur_state.attributes.get(ATTR_XY_COLOR):
color = cur_state.attributes.get(ATTR_XY_COLOR)
else:
color = random.choice(light_colors)
data = {
ATTR_BRIGHTNESS: service.data.get(ATTR_BRIGHTNESS, 200),
ATTR_XY_COLOR: color
}
else:
data = None
@ -124,7 +147,7 @@ def setup(hass, config):
})
# Setup chromecast
hass.states.set("chromecast.Living_Rm", "Netflix",
hass.states.set("chromecast.Living_Rm", "Plex",
{'friendly_name': 'Living Room',
ATTR_ENTITY_PICTURE:
'http://graph.facebook.com/KillBillMovie/picture'})
@ -141,4 +164,38 @@ def setup(hass, config):
'unit_of_measurement': '%'
})
# Nest demo
hass.states.set("thermostat.Nest", "23",
{
ATTR_UNIT_OF_MEASUREMENT: TEMP_CELCIUS,
ATTR_CURRENT_TEMPERATURE: '18',
ATTR_AWAY_MODE: STATE_OFF
})
configurator_ids = []
def hue_configuration_callback(data):
""" Fake callback, mark config as done. """
time.sleep(2)
# First time it is called, pretend it failed.
if len(configurator_ids) == 1:
configurator.notify_errors(
configurator_ids[0],
"Failed to register, please try again.")
configurator_ids.append(0)
else:
configurator.request_done(configurator_ids[0])
request_id = configurator.request_config(
hass, "Philips Hue", hue_configuration_callback,
description=("Press the button on the bridge to register Philips Hue "
"with Home Assistant."),
description_image="/static/images/config_philips_hue.jpg",
submit_caption="I have pressed the button"
)
configurator_ids.append(request_id)
return True

View File

@ -66,7 +66,6 @@ def setup(hass, config):
else:
return None
# pylint: disable=unused-argument
def schedule_light_on_sun_rise(entity, old_state, new_state):
"""The moment sun sets we want to have all the lights on.
We will schedule to have each light start after one another

View File

@ -24,9 +24,8 @@ DEPENDENCIES = []
SERVICE_DEVICE_TRACKER_RELOAD = "reload_devices_csv"
GROUP_NAME_ALL_DEVICES = 'all_devices'
ENTITY_ID_ALL_DEVICES = group.ENTITY_ID_FORMAT.format(
GROUP_NAME_ALL_DEVICES)
GROUP_NAME_ALL_DEVICES = 'all devices'
ENTITY_ID_ALL_DEVICES = group.ENTITY_ID_FORMAT.format('all_devices')
ENTITY_ID_FORMAT = DOMAIN + '.{}'
@ -111,26 +110,24 @@ class DeviceTracker(object):
""" Triggers update of the device states. """
self.update_devices(now)
# pylint: disable=unused-argument
dev_group = group.Group(
hass, GROUP_NAME_ALL_DEVICES, user_defined=False)
def reload_known_devices_service(service):
""" Reload known devices file. """
group.remove_group(self.hass, GROUP_NAME_ALL_DEVICES)
self._read_known_devices_file()
self.update_devices(datetime.now())
if self.tracked:
group.setup_group(
self.hass, GROUP_NAME_ALL_DEVICES,
self.device_entity_ids, False)
dev_group.update_tracked_entity_ids(self.device_entity_ids)
reload_known_devices_service(None)
if self.invalid_known_devices_file:
return
hass.track_time_change(update_device_state)
hass.track_time_change(
update_device_state, second=range(0, 60, 12))
hass.services.register(DOMAIN,
SERVICE_DEVICE_TRACKER_RELOAD,

View File

@ -17,7 +17,6 @@ MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
_LOGGER = logging.getLogger(__name__)
# pylint: disable=unused-argument
def get_scanner(hass, config):
""" Validates config and returns a Luci scanner. """
if not validate_config(config,
@ -101,7 +100,12 @@ class LuciDeviceScanner(object):
result = _req_json_rpc(url, 'net.arptable',
params={'auth': self.token})
if result:
self.last_results = [x['HW address'] for x in result]
self.last_results = []
for device_entry in result:
# Check if the Flags for each device contain
# NUD_REACHABLE and if so, add it to last_results
if int(device_entry['Flags'], 16) & 0x2:
self.last_results.append(device_entry['HW address'])
return True

View File

@ -14,7 +14,6 @@ MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
_LOGGER = logging.getLogger(__name__)
# pylint: disable=unused-argument
def get_scanner(hass, config):
""" Validates config and returns a Netgear scanner. """
if not validate_config(config,
@ -22,7 +21,10 @@ def get_scanner(hass, config):
_LOGGER):
return None
scanner = NetgearDeviceScanner(config[DOMAIN])
info = config[DOMAIN]
scanner = NetgearDeviceScanner(
info[CONF_HOST], info[CONF_USERNAME], info[CONF_PASSWORD])
return scanner if scanner.success_init else None
@ -30,10 +32,7 @@ def get_scanner(hass, config):
class NetgearDeviceScanner(object):
""" This class queries a Netgear wireless router using the SOAP-api. """
def __init__(self, config):
host = config[CONF_HOST]
username, password = config[CONF_USERNAME], config[CONF_PASSWORD]
def __init__(self, host, username, password):
self.last_results = []
try:
@ -54,32 +53,27 @@ class NetgearDeviceScanner(object):
self.lock = threading.Lock()
_LOGGER.info("Logging in")
if self._api.login():
self.success_init = True
self._update_info()
self.success_init = self._api.login()
if self.success_init:
self._update_info()
else:
_LOGGER.error("Failed to Login")
self.success_init = False
def scan_devices(self):
""" Scans for new devices and return a
list containing found device ids. """
self._update_info()
return [device.mac for device in self.last_results]
return (device.mac for device in self.last_results)
def get_device_name(self, mac):
""" Returns the name of the given device or None if we don't know. """
filter_named = [device.name for device in self.last_results
if device.mac == mac]
if filter_named:
return filter_named[0]
else:
try:
return next(device.name for device in self.last_results
if device.mac == mac)
except StopIteration:
return None
@Throttle(MIN_TIME_BETWEEN_SCANS)
@ -92,4 +86,4 @@ class NetgearDeviceScanner(object):
with self.lock:
_LOGGER.info("Scanning")
self.last_results = self._api.get_attached_devices()
self.last_results = self._api.get_attached_devices() or []

View File

@ -20,7 +20,6 @@ MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
_LOGGER = logging.getLogger(__name__)
# pylint: disable=unused-argument
def get_scanner(hass, config):
""" Validates config and returns a Nmap scanner. """
if not validate_config(config, {DOMAIN: [CONF_HOSTS]},

View File

@ -20,7 +20,6 @@ CONF_HTTP_ID = "http_id"
_LOGGER = logging.getLogger(__name__)
# pylint: disable=unused-argument
def get_scanner(hass, config):
""" Validates config and returns a Tomato scanner. """
if not validate_config(config,

View File

@ -0,0 +1,86 @@
"""
Starts a service to scan in intervals for new devices.
Will emit EVENT_PLATFORM_DISCOVERED whenever a new service has been discovered.
Knows which components handle certain types, will make sure they are
loaded before the EVENT_PLATFORM_DISCOVERED is fired.
"""
import logging
import threading
# pylint: disable=no-name-in-module, import-error
from homeassistant.external.netdisco.netdisco import DiscoveryService
import homeassistant.external.netdisco.netdisco.const as services
from homeassistant import bootstrap
from homeassistant.const import (
EVENT_HOMEASSISTANT_START, EVENT_PLATFORM_DISCOVERED,
ATTR_SERVICE, ATTR_DISCOVERED)
DOMAIN = "discovery"
DEPENDENCIES = []
SCAN_INTERVAL = 300 # seconds
SERVICE_HANDLERS = {
services.BELKIN_WEMO: "switch",
services.GOOGLE_CAST: "chromecast",
services.PHILIPS_HUE: "light",
}
def listen(hass, service, callback):
"""
Setup listener for discovery of specific service.
Service can be a string or a list/tuple.
"""
if isinstance(service, str):
service = (service,)
else:
service = tuple(service)
def discovery_event_listener(event):
""" Listens for discovery events. """
if event.data[ATTR_SERVICE] in service:
callback(event.data[ATTR_SERVICE], event.data[ATTR_DISCOVERED])
hass.bus.listen(EVENT_PLATFORM_DISCOVERED, discovery_event_listener)
def setup(hass, config):
""" Starts a discovery service. """
# Disable zeroconf logging, it spams
logging.getLogger('zeroconf').setLevel(logging.CRITICAL)
logger = logging.getLogger(__name__)
lock = threading.Lock()
def new_service_listener(service, info):
""" Called when a new service is found. """
with lock:
component = SERVICE_HANDLERS.get(service)
logger.info("Found new service: %s %s", service, info)
if component and component not in hass.components:
bootstrap.setup_component(hass, component, config)
hass.bus.fire(EVENT_PLATFORM_DISCOVERED, {
ATTR_SERVICE: service,
ATTR_DISCOVERED: info
})
def start_discovery(event):
""" Start discovering. """
netdisco = DiscoveryService(SCAN_INTERVAL)
netdisco.add_listener(new_service_listener)
netdisco.start()
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_discovery)
return True

View File

@ -5,12 +5,11 @@ homeassistant.components.groups
Provides functionality to group devices that can be turned on or off.
"""
import logging
import homeassistant as ha
import homeassistant.util as util
from homeassistant.const import (
ATTR_ENTITY_ID, STATE_ON, STATE_OFF, STATE_HOME, STATE_NOT_HOME)
ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, STATE_ON, STATE_OFF,
STATE_HOME, STATE_NOT_HOME, STATE_UNKNOWN)
DOMAIN = "group"
DEPENDENCIES = []
@ -22,8 +21,6 @@ ATTR_AUTO = "auto"
# List of ON/OFF state tuples for groupable states
_GROUP_TYPES = [(STATE_ON, STATE_OFF), (STATE_HOME, STATE_NOT_HOME)]
_GROUPS = {}
def _get_group_on_off(state):
""" Determine the group on/off states based on a state. """
@ -94,89 +91,97 @@ def get_entity_ids(hass, entity_id, domain_filter=None):
def setup(hass, config):
""" Sets up all groups found definded in the configuration. """
for name, entity_ids in config.get(DOMAIN, {}).items():
entity_ids = entity_ids.split(",")
setup_group(hass, name, entity_ids)
setup_group(hass, name, entity_ids.split(","))
return True
def setup_group(hass, name, entity_ids, user_defined=True):
""" Sets up a group state that is the combined state of
several states. Supports ON/OFF and DEVICE_HOME/DEVICE_NOT_HOME. """
logger = logging.getLogger(__name__)
class Group(object):
""" Tracks a group of entity ids. """
def __init__(self, hass, name, entity_ids=None, user_defined=True):
self.hass = hass
self.name = name
self.user_defined = user_defined
# In case an iterable is passed in
entity_ids = list(entity_ids)
self.entity_id = util.ensure_unique_string(
ENTITY_ID_FORMAT.format(util.slugify(name)),
hass.states.entity_ids(DOMAIN))
if not entity_ids:
logger.error(
'Error setting up group %s: no entities passed in to track', name)
self.tracking = []
self.group_on, self.group_off = None, None
return False
# Loop over the given entities to:
# - determine which group type this is (on_off, device_home)
# - determine which states exist and have groupable states
# - determine the current state of the group
warnings = []
group_ids = []
group_on, group_off = None, None
group_state = False
for entity_id in entity_ids:
state = hass.states.get(entity_id)
# Try to determine group type if we didn't yet
if group_on is None and state:
group_on, group_off = _get_group_on_off(state.state)
if group_on is None:
# We did not find a matching group_type
warnings.append(
"Entity {} has ungroupable state '{}'".format(
name, state.state))
continue
# Check if entity exists
if not state:
warnings.append("Entity {} does not exist".format(entity_id))
# Check if entity is invalid state
elif state.state != group_off and state.state != group_on:
warnings.append("State of {} is {} (expected: {} or {})".format(
entity_id, state.state, group_off, group_on))
# We have a valid group state
if entity_ids is not None:
self.update_tracked_entity_ids(entity_ids)
else:
group_ids.append(entity_id)
self.force_update()
# Keep track of the group state to init later on
group_state = group_state or state.state == group_on
@property
def state(self):
""" Return the current state from the group. """
return self.hass.states.get(self.entity_id)
# If none of the entities could be found during setup
if not group_ids:
logger.error('Unable to find any entities to track for group %s', name)
@property
def state_attr(self):
""" State attributes of this group. """
return {
ATTR_ENTITY_ID: self.tracking,
ATTR_AUTO: not self.user_defined,
ATTR_FRIENDLY_NAME: self.name
}
return False
def update_tracked_entity_ids(self, entity_ids):
""" Update the tracked entity IDs. """
self.stop()
self.tracking = tuple(entity_ids)
self.group_on, self.group_off = None, None
elif warnings:
logger.warning(
'Warnings during setting up group %s: %s',
name, ", ".join(warnings))
self.force_update()
group_entity_id = ENTITY_ID_FORMAT.format(util.slugify(name))
state = group_on if group_state else group_off
state_attr = {ATTR_ENTITY_ID: group_ids, ATTR_AUTO: not user_defined}
self.start()
# pylint: disable=unused-argument
def update_group_state(entity_id, old_state, new_state):
def force_update(self):
""" Query all the tracked states and update group state. """
for entity_id in self.tracking:
state = self.hass.states.get(entity_id)
if state is not None:
self._update_group_state(state.entity_id, None, state)
# If parsing the entitys did not result in a state, set UNKNOWN
if self.state is None:
self.hass.states.set(
self.entity_id, STATE_UNKNOWN, self.state_attr)
def start(self):
""" Starts the tracking. """
self.hass.states.track_change(self.tracking, self._update_group_state)
def stop(self):
""" Unregisters the group from Home Assistant. """
self.hass.states.remove(self.entity_id)
self.hass.bus.remove_listener(
ha.EVENT_STATE_CHANGED, self._update_group_state)
def _update_group_state(self, entity_id, old_state, new_state):
""" Updates the group state based on a state change by
a tracked entity. """
cur_gr_state = hass.states.get(group_entity_id).state
# We have not determined type of group yet
if self.group_on is None:
self.group_on, self.group_off = _get_group_on_off(new_state.state)
if self.group_on is not None:
# New state of the group is going to be based on the first
# state that we can recognize
self.hass.states.set(
self.entity_id, new_state.state, self.state_attr)
return
# There is already a group state
cur_gr_state = self.hass.states.get(self.entity_id).state
group_on, group_off = self.group_on, self.group_off
# if cur_gr_state = OFF and new_state = ON: set ON
# if cur_gr_state = ON and new_state = OFF: research
@ -184,31 +189,21 @@ def setup_group(hass, name, entity_ids, user_defined=True):
if cur_gr_state == group_off and new_state.state == group_on:
hass.states.set(group_entity_id, group_on, state_attr)
self.hass.states.set(
self.entity_id, group_on, self.state_attr)
elif cur_gr_state == group_on and new_state.state == group_off:
elif (cur_gr_state == group_on and
new_state.state == group_off):
# Check if any of the other states is still on
if not any([hass.states.is_state(ent_id, group_on)
for ent_id in group_ids
if entity_id != ent_id]):
hass.states.set(group_entity_id, group_off, state_attr)
_GROUPS[group_entity_id] = hass.states.track_change(
group_ids, update_group_state)
hass.states.set(group_entity_id, state, state_attr)
return True
if not any(self.hass.states.is_state(ent_id, group_on)
for ent_id in self.tracking if entity_id != ent_id):
self.hass.states.set(
self.entity_id, group_off, self.state_attr)
def remove_group(hass, name):
""" Remove a group and its state listener from Home Assistant. """
group_entity_id = ENTITY_ID_FORMAT.format(util.slugify(name))
def setup_group(hass, name, entity_ids, user_defined=True):
""" Sets up a group state that is the combined state of
several states. Supports ON/OFF and DEVICE_HOME/DEVICE_NOT_HOME. """
if hass.states.get(group_entity_id) is not None:
hass.states.remove(group_entity_id)
if group_entity_id in _GROUPS:
hass.bus.remove_listener(
ha.EVENT_STATE_CHANGED, _GROUPS.pop(group_entity_id))
return Group(hass, name, entity_ids, user_defined)

View File

@ -86,7 +86,7 @@ import homeassistant as ha
from homeassistant.const import (
SERVER_PORT, URL_API, URL_API_STATES, URL_API_EVENTS, URL_API_SERVICES,
URL_API_EVENT_FORWARD, URL_API_STATES_ENTITY, AUTH_HEADER)
from homeassistant.helpers import validate_config, TrackStates
from homeassistant.helpers import TrackStates
import homeassistant.remote as rem
import homeassistant.util as util
from . import frontend
@ -117,13 +117,18 @@ DATA_API_PASSWORD = 'api_password'
_LOGGER = logging.getLogger(__name__)
def setup(hass, config):
def setup(hass, config=None):
""" Sets up the HTTP API and debug interface. """
if not validate_config(config, {DOMAIN: [CONF_API_PASSWORD]}, _LOGGER):
return False
if config is None or DOMAIN not in config:
config = {DOMAIN: {}}
api_password = config[DOMAIN][CONF_API_PASSWORD]
api_password = config[DOMAIN].get(CONF_API_PASSWORD)
no_password_set = api_password is None
if no_password_set:
api_password = util.get_random_string()
# If no server host is given, accept all incoming requests
server_host = config[DOMAIN].get(CONF_SERVER_HOST, '0.0.0.0')
@ -132,19 +137,16 @@ def setup(hass, config):
development = config[DOMAIN].get(CONF_DEVELOPMENT, "") == "1"
server = HomeAssistantHTTPServer((server_host, server_port),
RequestHandler, hass, api_password,
development)
server = HomeAssistantHTTPServer(
(server_host, server_port), RequestHandler, hass, api_password,
development, no_password_set)
hass.bus.listen_once(
ha.EVENT_HOMEASSISTANT_START,
lambda event:
threading.Thread(target=server.start, daemon=True).start())
# If no local api set, set one with known information
if isinstance(hass, rem.HomeAssistant) and hass.local_api is None:
hass.local_api = \
rem.API(util.get_local_ip(), api_password, server_port)
hass.local_api = rem.API(util.get_local_ip(), api_password, server_port)
return True
@ -158,13 +160,14 @@ class HomeAssistantHTTPServer(ThreadingMixIn, HTTPServer):
# pylint: disable=too-many-arguments
def __init__(self, server_address, request_handler_class,
hass, api_password, development=False):
hass, api_password, development, no_password_set):
super().__init__(server_address, request_handler_class)
self.server_address = server_address
self.hass = hass
self.api_password = api_password
self.development = development
self.no_password_set = no_password_set
# We will lazy init this one if needed
self.event_forwarder = None
@ -273,10 +276,13 @@ class RequestHandler(SimpleHTTPRequestHandler):
"Error parsing JSON", HTTP_UNPROCESSABLE_ENTITY)
return
api_password = self.headers.get(AUTH_HEADER)
if self.server.no_password_set:
api_password = self.server.api_password
else:
api_password = self.headers.get(AUTH_HEADER)
if not api_password and DATA_API_PASSWORD in data:
api_password = data[DATA_API_PASSWORD]
if not api_password and DATA_API_PASSWORD in data:
api_password = data[DATA_API_PASSWORD]
if '_METHOD' in data:
method = data.pop('_METHOD')
@ -345,7 +351,6 @@ class RequestHandler(SimpleHTTPRequestHandler):
""" DELETE request handler. """
self._handle_request('DELETE')
# pylint: disable=unused-argument
def _handle_get_root(self, path_match, data):
""" Renders the debug interface. """
@ -360,6 +365,10 @@ class RequestHandler(SimpleHTTPRequestHandler):
else:
app_url = "frontend-{}.html".format(frontend.VERSION)
# auto login if no password was set, else check api_password param
auth = (self.server.api_password if self.server.no_password_set
else data.get('api_password', ''))
write(("<!doctype html>"
"<html>"
"<head><title>Home Assistant</title>"
@ -378,19 +387,16 @@ class RequestHandler(SimpleHTTPRequestHandler):
" src='/static/webcomponents.min.js'></script>"
"<link rel='import' href='/static/{}' />"
"<splash-login auth='{}'></splash-login>"
"</body></html>").format(app_url, data.get('api_password', '')))
"</body></html>").format(app_url, auth))
# pylint: disable=unused-argument
def _handle_get_api(self, path_match, data):
""" Renders the debug interface. """
self._json_message("API running.")
# pylint: disable=unused-argument
def _handle_get_api_states(self, path_match, data):
""" Returns a dict containing all entity ids and their state. """
self._write_json(self.server.hass.states.all())
# pylint: disable=unused-argument
def _handle_get_api_states_entity(self, path_match, data):
""" Returns the state of a specific entity. """
entity_id = path_match.group('entity_id')

View File

@ -1,2 +1,2 @@
""" DO NOT MODIFY. Auto-generated by build_frontend script """
VERSION = "78343829ea70bf07a9e939b321587122"
VERSION = "43699d5ec727d3444985a1028d21e0d9"

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

View File

@ -33,6 +33,8 @@
"paper-dropdown": "polymer/paper-dropdown#~0.5.2",
"paper-item": "polymer/paper-item#~0.5.2",
"moment": "~2.8.4",
"core-style": "polymer/core-style#~0.5.2"
"core-style": "polymer/core-style#~0.5.2",
"paper-slider": "polymer/paper-slider#~0.5.2",
"color-picker-element": "~0.0.2"
}
}

View File

@ -0,0 +1,28 @@
<script src="../bower_components/moment/moment.js"></script>
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../components/state-info.html">
<polymer-element name="state-card-configurator" attributes="stateObj api" noscript>
<template>
<style>
.state {
margin-left: 16px;
text-transform: capitalize;
font-weight: 300;
font-size: 1.3rem;
text-align: right;
}
</style>
<div horizontal justified layout>
<state-info stateObj="{{stateObj}}"></state-info>
<div class='state'>{{stateObj.stateDisplay}}</div>
</div>
<!-- pre load the image so the dialog is rendered the proper size -->
<template if="{{stateObj.attributes.description_image}}">
<img hidden src="{{stateObj.attributes.description_image}}" />
</template>
</template>
</polymer-element>

View File

@ -0,0 +1,32 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="state-card-display.html">
<link rel="import" href="state-card-toggle.html">
<link rel="import" href="state-card-thermostat.html">
<link rel="import" href="state-card-configurator.html">
<polymer-element name="state-card-content" attributes="api stateObj">
<template>
<style>
:host {
display: block;
}
</style>
<div id='card'></div>
</template>
<script>
Polymer({
stateObjChanged: function() {
while (this.$.card.lastChild) {
this.$.card.removeChild(this.$.card.lastChild);
}
var stateCard = document.createElement("state-card-" + this.stateObj.cardType);
stateCard.api = this.api;
stateCard.stateObj = this.stateObj;
this.$.card.appendChild(stateCard);
}
});
</script>
</polymer-element>

View File

@ -0,0 +1,23 @@
<script src="../bower_components/moment/moment.js"></script>
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../components/state-info.html">
<polymer-element name="state-card-display" attributes="stateObj" noscript>
<template>
<style>
.state {
margin-left: 16px;
text-transform: capitalize;
font-weight: 300;
font-size: 1.3rem;
text-align: right;
}
</style>
<div horizontal justified layout>
<state-info stateObj="{{stateObj}}"></state-info>
<div class='state'>{{stateObj.stateDisplay}}</div>
</div>
</template>
</polymer-element>

View File

@ -0,0 +1,42 @@
<script src="../bower_components/moment/moment.js"></script>
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../components/state-info.html">
<polymer-element name="state-card-thermostat" attributes="stateObj api">
<template>
<style>
.state {
margin-left: 16px;
text-align: right;
}
.target {
text-transform: capitalize;
font-weight: 300;
font-size: 1.3rem;
}
.current {
color: darkgrey;
margin-top: -2px;
}
</style>
<div horizontal justified layout>
<state-info stateObj="{{stateObj}}"></state-info>
<div class='state'>
<div class='target'>
{{stateObj.stateDisplay}}
</div>
<div class='current'>
Currently: {{stateObj.attributes.current_temperature}} {{stateObj.attributes.unit_of_measurement}}
</div>
</div>
</div>
</template>
<script>
Polymer({});
</script>
</polymer-element>

View File

@ -0,0 +1,92 @@
<script src="../bower_components/moment/moment.js"></script>
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../bower_components/paper-toggle-button/paper-toggle-button.html">
<link rel="import" href="../components/state-info.html">
<polymer-element name="state-card-toggle" attributes="stateObj api">
<template>
<style>
/* the splash while enabling */
paper-toggle-button::shadow paper-radio-button::shadow #ink[checked] {
color: #0091ea;
}
/* filling of circle when checked */
paper-toggle-button::shadow paper-radio-button::shadow #onRadio {
background-color: #039be5;
}
/* line when checked */
paper-toggle-button::shadow #toggleBar[checked] {
background-color: #039be5;
}
</style>
<div horizontal justified layout>
<state-info flex stateObj="{{stateObj}}"></state-info>
<paper-toggle-button self-center
checked="{{toggleChecked}}"
on-click="{{toggleClicked}}">
</paper-toggle-button>
</div>
</template>
<script>
Polymer({
toggleChecked: -1,
observe: {
'stateObj.state': 'stateChanged'
},
// prevent the event from propegating
toggleClicked: function(ev) {
ev.stopPropagation();
},
toggleCheckedChanged: function(oldVal, newVal) {
// to filter out init
if(oldVal === -1) {
return;
}
if(newVal && this.stateObj.state == "off") {
this.turn_on();
} else if(!newVal && this.stateObj.state == "on") {
this.turn_off();
}
},
stateChanged: function(oldVal, newVal) {
this.toggleChecked = newVal === "on";
},
turn_on: function() {
// We call stateChanged after a successful call to re-sync the toggle
// with the state. It will be out of sync if our service call did not
// result in the entity to be turned on. Since the state is not changing,
// the resync is not called automatic.
this.api.turn_on(this.stateObj.entity_id, {
success: function() {
this.stateChanged(this.stateObj.state, this.stateObj.state);
}.bind(this)
});
},
turn_off: function() {
// We call stateChanged after a successful call to re-sync the toggle
// with the state. It will be out of sync if our service call did not
// result in the entity to be turned on. Since the state is not changing,
// the resync is not called automatic.
this.api.turn_off(this.stateObj.entity_id, {
success: function() {
this.stateChanged(this.stateObj.state, this.stateObj.state);
}.bind(this)
});
},
});
</script>
</polymer-element>

View File

@ -0,0 +1,33 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="state-card-content.html">
<polymer-element name="state-card" attributes="api stateObj" on-click="cardClicked">
<template>
<style>
:host {
border-radius: 2px;
box-shadow: rgba(0, 0, 0, 0.098) 0px 2px 4px, rgba(0, 0, 0, 0.098) 0px 0px 3px;
transition: all 0.30s ease-out;
position: relative;
background-color: white;
padding: 16px;
width: 100%;
cursor: pointer;
}
</style>
<state-card-content stateObj={{stateObj}} api={{api}}></state-card-content>
</template>
<script>
Polymer({
cardClicked: function() {
this.api.showmoreInfoDialog(this.stateObj.entity_id);
},
});
</script>
</polymer-element>

View File

@ -1,9 +1,11 @@
<link rel="import" href="bower_components/polymer/polymer.html">
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="bower_components/core-icon/core-icon.html">
<link rel="import" href="bower_components/core-icons/social-icons.html">
<link rel="import" href="bower_components/core-icons/image-icons.html">
<link rel="import" href="bower_components/core-icons/hardware-icons.html">
<link rel="import" href="../bower_components/core-icon/core-icon.html">
<link rel="import" href="../bower_components/core-icons/social-icons.html">
<link rel="import" href="../bower_components/core-icons/image-icons.html">
<link rel="import" href="../bower_components/core-icons/hardware-icons.html">
<link rel="import" href="../resources/home-assistant-icons.html">
<polymer-element name="domain-icon"
attributes="domain state" constructor="DomainIcon">
@ -51,6 +53,18 @@
case "simple_alarm":
return "social:notifications";
case "notify":
return "announcement";
case "thermostat":
return "homeassistant:thermostat";
case "sensor":
return "visibility";
case "configurator":
return "settings";
default:
return "bookmark-outline";
}

View File

@ -1,4 +1,4 @@
<link rel="import" href="bower_components/polymer/polymer.html">
<link rel="import" href="../bower_components/polymer/polymer.html">
<polymer-element name="entity-list" attributes="api cbEntityClicked">
<template>

View File

@ -1,4 +1,4 @@
<link rel="import" href="bower_components/polymer/polymer.html">
<link rel="import" href="../bower_components/polymer/polymer.html">
<polymer-element name="events-list" attributes="api cbEventClicked">
<template>

View File

@ -1,7 +1,7 @@
<link rel="import" href="bower_components/polymer/polymer.html">
<link rel="import" href="bower_components/core-menu/core-menu.html">
<link rel="import" href="bower_components/core-menu/core-submenu.html">
<link rel="import" href="bower_components/core-item/core-item.html">
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../bower_components/core-menu/core-menu.html">
<link rel="import" href="../bower_components/core-menu/core-submenu.html">
<link rel="import" href="../bower_components/core-item/core-item.html">
<link rel="import" href="domain-icon.html">

View File

@ -1,5 +1,5 @@
<link rel="import" href="bower_components/polymer/polymer.html">
<link rel="import" href="bower_components/core-image/core-image.html">
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../bower_components/core-image/core-image.html">
<link rel="import" href="domain-icon.html">

View File

@ -0,0 +1,83 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../cards/state-card.html">
<polymer-element name="state-cards" attributes="api filter">
<template>
<style>
:host {
display: block;
width: 100%;
}
@media all and (min-width: 764px) {
:host {
padding-bottom: 8px;
}
.state-card {
width: calc(50% - 44px);
margin: 8px 0 0 8px;
}
}
@media all and (min-width: 1100px) {
.state-card {
width: calc(33% - 38px);
}
}
@media all and (min-width: 1450px) {
.state-card {
width: calc(25% - 42px);
}
}
.no-states-content {
max-width: 500px;
background-color: #fff;
border-radius: 2px;
box-shadow: rgba(0, 0, 0, 0.098) 0px 2px 4px, rgba(0, 0, 0, 0.098) 0px 0px 3px;
padding: 16px;
margin: 0 auto;
}
</style>
<div horizontal layout wrap>
<template repeat="{{states as state}}">
<state-card class="state-card" stateObj={{state}} api={{api}}></state-card>
</template>
<template if="{{states.length == 0}}">
<div class='no-states-content'>
<content></content>
</div>
</template>
</div>
</template>
<script>
Polymer({
filter: null,
states: [],
observe: {
'api.states': 'filterChanged'
},
filterChanged: function() {
if(this.filter === 'customgroup') {
this.states = this.api.getCustomGroups();
} else {
// if no filter, return all non-group states
this.states = this.api.states.filter(function(state) {
return state.domain != 'group';
});
}
},
});
</script>
</polymer-element>

View File

@ -0,0 +1,47 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../bower_components/core-tooltip/core-tooltip.html">
<link rel="import" href="../bower_components/core-style/core-style.html">
<link rel="import" href="state-badge.html">
<polymer-element name="state-info" attributes="stateObj" noscript>
<template>
<style>
state-badge {
float: left;
}
.name {
text-transform: capitalize;
font-weight: 300;
font-size: 1.3rem;
}
.info {
margin-left: 60px;
}
.time-ago {
color: darkgrey;
margin-top: -2px;
}
</style>
<div>
<state-badge stateObj="{{stateObj}}"></state-badge>
<div class='info'>
<div class='name'>
{{stateObj.entityDisplay}}
</div>
<div class="time-ago">
<core-tooltip label="{{stateObj.last_changed}}" position="bottom">
{{stateObj.relativeLastChanged}}
</core-tooltip>
</div>
</div>
</div>
</template>
</polymer-element>

View File

@ -0,0 +1,89 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../bower_components/paper-button/paper-button.html">
<link rel="import" href="../bower_components/paper-input/paper-input.html">
<link rel="import" href="../bower_components/paper-input/paper-input-decorator.html">
<link rel="import" href="../bower_components/paper-input/paper-autogrow-textarea.html">
<link rel="import" href="ha-action-dialog.html">
<link rel="import" href="../components/events-list.html">
<polymer-element name="event-fire-dialog" attributes="api">
<template>
<ha-action-dialog
id="dialog"
heading="Fire Event"
class='two-column'
closeSelector='[dismissive]'>
<div layout horizontal>
<div class='ha-form'>
<paper-input
id="inputType" label="Event Type" floatingLabel="true"
autofocus required></paper-input>
<paper-input-decorator
label="Event Data (JSON, optional)"
floatingLabel="true">
<!--
<paper-autogrow-textarea id="inputDataWrapper">
<textarea id="inputData"></textarea>
</paper-autogrow-textarea>
-->
<textarea id="inputData" rows="5"></textarea>
</paper-input-decorator>
</div>
<div class='sidebar'>
<b>Available events:</b>
<events-list api={{api}} cbEventClicked={{eventSelected}}></event-list>
</div>
</div>
<paper-button dismissive>Cancel</paper-button>
<paper-button affirmative on-click={{clickFireEvent}}>Fire Event</paper-button>
</ha-action-dialog>
</template>
<script>
Polymer({
ready: function() {
// to ensure callback methods work..
this.eventSelected = this.eventSelected.bind(this);
},
show: function(eventType, eventData) {
this.setEventType(eventType);
this.setEventData(eventData);
this.job('showDialogAfterRender', function() {
this.$.dialog.toggle();
}.bind(this));
},
setEventType: function(eventType) {
this.$.inputType.value = eventType;
},
setEventData: function(eventData) {
this.$.inputData.value = eventData;
// this.$.inputDataWrapper.update();
},
eventSelected: function(eventType) {
this.setEventType(eventType);
},
clickFireEvent: function() {
try {
this.api.fire_event(
this.$.inputType.value,
this.$.inputData.value ? JSON.parse(this.$.inputData.value) : {});
this.$.dialog.close();
} catch (err) {
alert("Error parsing JSON: " + err);
}
}
});
</script>
</polymer-element>

View File

@ -0,0 +1,18 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../bower_components/core-style/core-style.html">
<link rel="import" href="../bower_components/paper-dialog/paper-action-dialog.html">
<link rel="import" href="../bower_components/paper-dialog/paper-dialog-transition.html">
<polymer-element name="ha-action-dialog" extends="paper-action-dialog">
<template>
<core-style ref='ha-dialog'></core-style>
<shadow></shadow>
</template>
<script>
Polymer({
layered: true,
backdrop: true,
transition: 'core-transition-bottom',
});
</script>
</polymer-element>

View File

@ -0,0 +1,62 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="ha-action-dialog.html">
<link rel="import" href="../cards/state-card-content.html">
<link rel="import" href="../more-infos/more-info-content.html">
<polymer-element name="more-info-dialog" attributes="api">
<template>
<ha-action-dialog id="dialog">
<style>
.title-card {
margin-bottom: 24px;
}
</style>
<div>
<state-card-content stateObj="{{stateObj}}" api="{{api}}" class='title-card'>
</state-card-content>
<more-info-content stateObj="{{stateObj}}" api="{{api}}"></more-info-content>
</div>
<paper-button dismissive on-click={{editClicked}}>Debug</paper-button>
<paper-button affirmative>Dismiss</paper-button>
</ha-action-dialog>
</template>
<script>
Polymer({
stateObj: {},
observe: {
'stateObj.attributes': 'reposition'
},
/**
* Whenever the attributes change, the more info component can
* hide or show elements. We will reposition the dialog.
*/
reposition: function(oldVal, newVal) {
// Only resize if already open
if(this.$.dialog.opened) {
this.job('resizeAfterLayoutChange', function() {
this.$.dialog.resizeHandler();
}.bind(this), 1000);
}
},
show: function(stateObj) {
this.stateObj = stateObj;
this.job('showDialogAfterRender', function() {
this.$.dialog.toggle();
}.bind(this));
},
editClicked: function(ev) {
this.api.showEditStateDialog(this.stateObj.entity_id);
}
});
</script>
</polymer-element>

View File

@ -0,0 +1,87 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../bower_components/paper-button/paper-button.html">
<link rel="import" href="../bower_components/paper-input/paper-input.html">
<link rel="import" href="../bower_components/paper-input/paper-input-decorator.html">
<link rel="import" href="../bower_components/paper-input/paper-autogrow-textarea.html">
<link rel="import" href="ha-action-dialog.html">
<link rel="import" href="../components/services-list.html">
<polymer-element name="service-call-dialog" attributes="api">
<template>
<ha-action-dialog
id="dialog"
heading="Call Service"
closeSelector='[dismissive]'>
<core-style ref='ha-dialog'></core-style>
<div layout horizontal>
<div class='ha-form'>
<paper-input id="inputDomain" label="Domain" floatingLabel="true" autofocus required></paper-input>
<paper-input id="inputService" label="Service" floatingLabel="true" required></paper-input>
<paper-input-decorator
label="Service Data (JSON, optional)"
floatingLabel="true">
<!--
<paper-autogrow-textarea id="inputDataWrapper">
<textarea id="inputData"></textarea>
</paper-autogrow-textarea>
-->
<textarea id="inputData" rows="5"></textarea>
</paper-input-decorator>
</div>
<div class='sidebar'>
<b>Available services:</b>
<services-list api={{api}} cbServiceClicked={{serviceSelected}}></event-list>
</div>
</div>
<paper-button dismissive>Cancel</paper-button>
<paper-button affirmative on-click={{clickCallService}}>Call Service</paper-button>
</ha-action-dialog>
</template>
<script>
Polymer({
ready: function() {
// to ensure callback methods work..
this.serviceSelected = this.serviceSelected.bind(this);
},
show: function(domain, service, serviceData) {
this.setService(domain, service);
this.$.inputData.value = serviceData;
// this.$.inputDataWrapper.update();
this.job('showDialogAfterRender', function() {
this.$.dialog.toggle();
}.bind(this));
},
setService: function(domain, service) {
this.$.inputDomain.value = domain;
this.$.inputService.value = service;
},
serviceSelected: function(domain, service) {
this.setService(domain, service);
},
clickCallService: function() {
try {
this.api.call_service(
this.$.inputDomain.value,
this.$.inputService.value,
this.$.inputData.value ? JSON.parse(this.$.inputData.value) : {});
this.$.dialog.close();
} catch (err) {
alert("Error parsing JSON: " + err);
}
}
});
</script>
</polymer-element>

View File

@ -0,0 +1,105 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../bower_components/paper-button/paper-button.html">
<link rel="import" href="../bower_components/paper-input/paper-input.html">
<link rel="import" href="../bower_components/paper-input/paper-input-decorator.html">
<link rel="import" href="../bower_components/paper-input/paper-autogrow-textarea.html">
<link rel="import" href="ha-action-dialog.html">
<link rel="import" href="../components/entity-list.html">
<polymer-element name="state-set-dialog" attributes="api">
<template>
<ha-action-dialog
id="dialog"
heading="Set State"
closeSelector='[dismissive]'>
<core-style ref='ha-dialog'></core-style>
<p>
This dialog will update the representation of the device within Home Assistant.<br />
This will not communicate with the actual device.
</p>
<div layout horizontal>
<div class='ha-form'>
<paper-input id="inputEntityID" label="Entity ID" floatingLabel="true" autofocus required></paper-input>
<paper-input id="inputState" label="State" floatingLabel="true" required></paper-input>
<paper-input-decorator
label="State attributes (JSON, optional)"
floatingLabel="true">
<!--
<paper-autogrow-textarea id="inputDataWrapper">
<textarea id="inputData"></textarea>
</paper-autogrow-textarea>
-->
<textarea id="inputData" rows="5"></textarea>
</paper-input-decorator>
</div>
<div class='sidebar'>
<b>Current entities:</b>
<entity-list api={{api}} cbEntityClicked={{entitySelected}}></entity-list>
</div>
</div>
<paper-button dismissive>Cancel</paper-button>
<paper-button affirmative on-click={{clickSetState}}>Set State</paper-button>
</ha-action-dialog>
</template>
<script>
Polymer({
ready: function() {
// to ensure callback methods work..
this.entitySelected = this.entitySelected.bind(this);
},
show: function(entityId, state, stateData) {
this.setEntityId(entityId);
this.setState(state);
this.setStateData(stateData);
this.job('showDialogAfterRender', function() {
this.$.dialog.toggle();
}.bind(this));
},
setEntityId: function(entityId) {
this.$.inputEntityID.value = entityId;
},
setState: function(state) {
this.$.inputState.value = state;
},
setStateData: function(stateData) {
var value = stateData ? JSON.stringify(stateData, null, ' ') : "";
this.$.inputData.value = value;
},
entitySelected: function(entityId) {
this.setEntityId(entityId);
var state = this.api.getState(entityId);
this.setState(state.state);
this.setStateData(state.attributes);
},
clickSetState: function(ev) {
try {
this.api.set_state(
this.$.inputEntityID.value,
this.$.inputState.value,
this.$.inputData.value ? JSON.parse(this.$.inputData.value) : {}
);
this.$.dialog.close();
} catch (err) {
alert("Error parsing JSON: " + err);
}
}
});
</script>
</polymer-element>

View File

@ -1,109 +0,0 @@
<link rel="import" href="bower_components/polymer/polymer.html">
<link rel="import" href="bower_components/paper-dialog/paper-action-dialog.html">
<link rel="import" href="bower_components/paper-button/paper-button.html">
<link rel="import" href="bower_components/paper-input/paper-input.html">
<link rel="import" href="bower_components/paper-input/paper-input-decorator.html">
<link rel="import" href="bower_components/paper-input/paper-autogrow-textarea.html">
<link rel="import" href="events-list.html">
<polymer-element name="event-fire-dialog" attributes="api">
<template>
<paper-action-dialog id="dialog" heading="Fire Event" transition="core-transition-bottom" backdrop>
<style>
:host {
font-family: RobotoDraft, 'Helvetica Neue', Helvetica, Arial;
}
paper-input {
display: block;
}
paper-input:first-child {
padding-top: 0;
}
.eventContainer {
margin-left: 30px;
}
@media all and (max-width: 620px) {
paper-action-dialog {
margin: 0;
width: 100%;
height: calc(100% - 64px);
top: 64px;
}
.eventContainer {
display: none;
}
}
</style>
<div layout horizontal>
<div>
<paper-input id="inputType" label="Event Type" floatingLabel="true" autofocus required></paper-input>
<paper-input-decorator
label="Event Data (JSON, optional)"
floatingLabel="true">
<!--
<paper-autogrow-textarea id="inputDataWrapper">
<textarea id="inputData"></textarea>
</paper-autogrow-textarea>
-->
<textarea id="inputData" rows="5"></textarea>
</paper-input-decorator>
</div>
<div class='eventContainer'>
<b>Available events:</b>
<events-list api={{api}} cbEventClicked={{eventSelected}}></event-list>
</div>
</div>
<paper-button dismissive>Cancel</paper-button>
<paper-button affirmative on-click={{clickFireEvent}}>Fire Event</paper-button>
</paper-dialog>
</template>
<script>
Polymer({
ready: function() {
// to ensure callback methods work..
this.eventSelected = this.eventSelected.bind(this)
},
show: function(eventType, eventData) {
this.setEventType(eventType);
this.setEventData(eventData);
this.$.dialog.toggle();
},
setEventType: function(eventType) {
this.$.inputType.value = eventType;
},
setEventData: function(eventData) {
this.$.inputData.value = eventData;
// this.$.inputDataWrapper.update();
},
eventSelected: function(eventType) {
this.setEventType(eventType);
},
clickFireEvent: function() {
var data;
if(this.$.inputData.value != "") {
data = JSON.parse(this.$.inputData.value);
} else {
data = {};
}
this.api.fire_event(this.$.inputType.value, data);
}
});
</script>
</polymer-element>

View File

@ -1,18 +1,46 @@
<script src="bower_components/moment/moment.js"></script>
<link rel="import" href="bower_components/polymer/polymer.html">
<link rel="import" href="bower_components/paper-toast/paper-toast.html">
<link rel="import" href="event-fire-dialog.html">
<link rel="import" href="service-call-dialog.html">
<link rel="import" href="state-set-dialog.html">
<link rel="import" href="dialogs/event-fire-dialog.html">
<link rel="import" href="dialogs/service-call-dialog.html">
<link rel="import" href="dialogs/state-set-dialog.html">
<link rel="import" href="dialogs/more-info-dialog.html">
<script>
var ha = {};
ha.util = {};
ha.util.parseTime = function(timeString) {
return moment(timeString, "HH:mm:ss DD-MM-YYYY");
};
ha.util.relativeTime = function(timeString) {
return ha.util.parseTime(timeString).fromNow();
};
PolymerExpressions.prototype.relativeHATime = function(timeString) {
return ha.util.relativeTime(timeString);
};
PolymerExpressions.prototype.HATimeStripDate = function(timeString) {
return (timeString || "").split(' ')[0];
};
</script>
<polymer-element name="home-assistant-api" attributes="auth">
<template>
<paper-toast id="toast" role="alert" text=""></paper-toast>
<event-fire-dialog id="eventDialog" api={{api}}></event-fire-dialog>
<service-call-dialog id="serviceDialog" api={{api}}></service-call-dialog>
<state-set-dialog id="stateDialog" api={{api}}></state-set-dialog>
<state-set-dialog id="stateSetDialog" api={{api}}></state-set-dialog>
<more-info-dialog id="moreInfoDialog" api={{api}}></more-info-dialog>
</template>
<script>
var domainsWithCard = ['thermostat', 'configurator'];
var domainsWithMoreInfo = ['light', 'group', 'sun', 'configurator'];
State = function(json, api) {
this.api = api;
@ -22,12 +50,12 @@
this.entity_id = json.entity_id;
var parts = json.entity_id.split(".");
this.domain = parts[0];
this.entity = parts[1];
this.object_id = parts[1];
if(this.attributes.friendly_name) {
this.entityDisplay = this.attributes.friendly_name;
} else {
this.entityDisplay = this.entity.replace(/_/g, " ");
this.entityDisplay = this.object_id.replace(/_/g, " ");
}
this.state = json.state;
@ -64,13 +92,33 @@
// how to render the card for this state
cardType: {
get: function() {
if(this.canToggle) {
if(domainsWithCard.indexOf(this.domain) !== -1) {
return this.domain;
} else if(this.canToggle) {
return "toggle";
} else {
return "display";
}
}
}
},
// how to render the more info of this state
moreInfoType: {
get: function() {
if(domainsWithMoreInfo.indexOf(this.domain) !== -1) {
return this.domain;
} else {
return 'default';
}
}
},
relativeLastChanged: {
get: function() {
return ha.util.relativeTime(this.last_changed);
}
},
});
Polymer({
@ -89,6 +137,14 @@
},
// local methods
removeState: function(entityId) {
var state = this.getState(entityId);
if (state !== null) {
this.states.splice(this.states.indexOf(state), 1);
}
},
getState: function(entityId) {
var found = this.states.filter(function(state) {
return state.entity_id == entityId;
@ -97,6 +153,24 @@
return found.length > 0 ? found[0] : null;
},
getStates: function(entityIds) {
var states = [];
var state;
for(var i = 0; i < entityIds.length; i++) {
state = this.getState(entityIds[i]);
if(state !== null) {
states.push(state);
}
}
return states;
},
getEntityIDs: function() {
return this.states.map(
function(state) { return state.entity_id; });
},
hasService: function(domain, service) {
var found = this.services.filter(function(serv) {
return serv.domain == domain && serv.services.indexOf(service) !== -1;
@ -105,6 +179,10 @@
return found.length > 0;
},
getCustomGroups: function() {
return this.states.filter(function(state) { return state.isCustomGroup;});
},
_laterFetchStates: function() {
if(this.stateUpdateTimeout) {
clearTimeout(this.stateUpdateTimeout);
@ -114,8 +192,8 @@
this.stateUpdateTimeout = setTimeout(this.fetchStates.bind(this), 60000);
},
_sortStates: function(states) {
states.sort(function(one, two) {
_sortStates: function() {
this.states.sort(function(one, two) {
if (one.entity_id > two.entity_id) {
return 1;
} else if (one.entity_id < two.entity_id) {
@ -126,34 +204,62 @@
});
},
/**
* Pushes a new state to the state machine.
* Will resort the states after a push and fire states-updated event.
*/
_pushNewState: function(new_state) {
var state;
var stateFound = false;
for(var i = 0; i < this.states.length; i++) {
if(this.states[i].entity_id == new_state.entity_id) {
state = this.states[i];
state.attributes = new_state.attributes;
state.last_changed = new_state.last_changed;
state.state = new_state.state;
stateFound = true;
break;
}
}
if(!stateFound) {
this.states.push(new State(new_state, this));
this._sortStates(this.states);
if (this.__pushNewState(new_state)) {
this._sortStates();
}
this.fire('states-updated');
},
_pushNewStates: function(new_states) {
new_states.map(function(state) {
this._pushNewState(state);
/**
* Creates or updates a state. Returns if a new state was added.
*/
__pushNewState: function(new_state) {
var curState = this.getState(new_state.entity_id);
if (curState === null) {
this.states.push(new State(new_state, this));
return true;
} else {
curState.attributes = new_state.attributes;
curState.last_changed = new_state.last_changed;
curState.state = new_state.state;
return false;
}
},
_pushNewStates: function(newStates, removeNonPresent) {
removeNonPresent = !!removeNonPresent;
var currentEntityIds = removeNonPresent ? this.getEntityIDs() : [];
var hasNew = newStates.reduce(function(hasNew, newState) {
var isNewState = this.__pushNewState(newState);
if (isNewState) {
return true;
} else if(removeNonPresent) {
currentEntityIds.splice(currentEntityIds.indexOf(newState.entity_id), 1);
}
return hasNew;
}.bind(this), false);
currentEntityIds.forEach(function(entityId) {
this.removeState(entityId);
}.bind(this));
if (hasNew) {
this._sortStates();
}
this.fire('states-updated');
},
// call api methods
@ -173,13 +279,7 @@
fetchStates: function(onSuccess, onError) {
var successStatesUpdate = function(newStates) {
this._sortStates(newStates);
this.states = newStates.map(function(json) {
return new State(json, this);
}.bind(this));
this.fire('states-updated');
this._pushNewStates(newStates, true);
this._laterFetchStates();
@ -330,6 +430,10 @@
},
// show dialogs
showmoreInfoDialog: function(entityId) {
this.$.moreInfoDialog.show(this.getState(entityId));
},
showEditStateDialog: function(entityId) {
var state = this.getState(entityId);
@ -341,7 +445,7 @@
state = state || "";
stateAttributes = stateAttributes || null;
this.$.stateDialog.show(entityId, state, stateAttributes);
this.$.stateSetDialog.show(entityId, state, stateAttributes);
},
showFireEventDialog: function(eventType, eventData) {

View File

@ -8,7 +8,7 @@
<link rel="import" href="bower_components/core-menu/core-menu.html">
<link rel="import" href="bower_components/paper-item/paper-item.html">
<link rel="import" href="states-cards.html">
<link rel="import" href="components/state-cards.html">
<polymer-element name="home-assistant-main" attributes="api">
<template>
@ -94,43 +94,52 @@
</paper-dropdown>
</paper-menu-button>
<div class="bottom fit" horizontal layout>
<paper-tabs id="tabsHolder" noink flex
selected="0" on-core-select="{{tabClicked}}">
<paper-tab>ALL</paper-tab>
<template if="{{hasCustomGroups}}">
<div class="bottom fit" horizontal layout>
<paper-tabs id="tabsHolder" noink flex
selected="0" on-core-select="{{tabClicked}}">
<template repeat="{{group in customGroups}}">
<paper-tab data-entity="{{group.entity_id}}">
{{group.entityDisplay}}
</paper-tab>
</template>
</paper-tabs>
</div>
<paper-tab>ALL</paper-tab>
<paper-tab data-filter='customgroup'>GROUPS</paper-tab>
</paper-tabs>
</div>
</template>
</core-toolbar>
<states-cards
<state-cards
api="{{api}}"
filter="{{selectedTab}}"
class="content"></states-cards>
filter="{{selectedFilter}}"
class="content">
<h3>Hi there!</h3>
<p>
It looks like we have nothing to show you right now. It could be that we have not yet discovered all your devices but it is more likely that you have not configured Home Assistant yet.
</p>
<p>
Please see the <a href='https://home-assistant.io/getting-started/' target='_blank'>Getting Started</a> section on how to setup your devices.
</p>
</state-cards>
</core-header-panel>
</template>
<script>
Polymer({
selectedTab: null,
selectedFilter: null,
hasCustomGroups: false,
computed: {
customGroups: "getCustomGroups(api.states)",
hasCustomGroups: "customGroups.length > 0"
observe: {
'api.states': 'updateHasCustomGroup'
},
// computed: {
// hasCustomGroups: "api.getCustomGroups().length > 0"
// },
tabClicked: function(ev) {
if(ev.detail.isSelected) {
// will be null for ALL tab
this.selectedTab = ev.detail.item.getAttribute('data-entity');
this.selectedFilter = ev.detail.item.getAttribute('data-filter');
}
},
@ -154,9 +163,8 @@
this.api.logOut();
},
getCustomGroups: function(states) {
return states ?
states.filter(function(state) { return state.isCustomGroup;}) : [];
updateHasCustomGroup: function() {
this.hasCustomGroups = this.api.getCustomGroups().length > 0;
}
});

View File

@ -0,0 +1,91 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../bower_components/paper-button/paper-button.html">
<link rel="import" href="../bower_components/paper-spinner/paper-spinner.html">
<polymer-element name="more-info-configurator" attributes="stateObj api">
<template>
<style>
p {
margin: 8px 0;
}
p > img {
max-width: 100%;
}
p.center {
text-align: center;
}
p.error {
color: #C62828;
}
p.submit {
text-align: center;
height: 41px;
}
p.submit paper-spinner {
margin-right: 16px;
}
p.submit span {
display: inline-block;
vertical-align: top;
margin-top: 6px;
}
</style>
<div layout vertical>
<template if="{{stateObj.state == 'configure'}}">
<p hidden?="{{!stateObj.attributes.description}}">
{{stateObj.attributes.description}}
</p>
<p class='error' hidden?="{{!stateObj.attributes.errors}}">
{{stateObj.attributes.errors}}
</p>
<p class='center' hidden?="{{!stateObj.attributes.description_image}}">
<img src='{{stateObj.attributes.description_image}}' />
</p>
<p class='submit'>
<paper-button raised on-click="{{submitClicked}}"
hidden?="{{action !== 'display'}}">
{{stateObj.attributes.submit_caption || "Set configuration"}}
</paper-button>
<span hidden?="{{action !== 'configuring'}}">
<paper-spinner active="true"></paper-spinner><span>Configuring…</span>
</span>
</p>
</template>
</div>
</template>
<script>
Polymer({
action: "display",
submitClicked: function() {
this.action = "configuring";
var data = {
configure_id: this.stateObj.attributes.configure_id
};
this.api.call_service('configurator', 'configure', data, {
success: function() {
this.action = 'display';
this.api.fetchAll();
}.bind(this),
error: function() {
this.action = 'display';
}.bind(this)
});
}
});
</script>
</polymer-element>

View File

@ -0,0 +1,45 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="more-info-default.html">
<link rel="import" href="more-info-light.html">
<link rel="import" href="more-info-group.html">
<link rel="import" href="more-info-sun.html">
<link rel="import" href="more-info-configurator.html">
<polymer-element name="more-info-content" attributes="api stateObj">
<template>
<style>
:host {
display: block;
}
</style>
<div id='moreInfo' class='{{classNames}}'></div>
</template>
<script>
Polymer({
classNames: '',
observe: {
'stateObj.attributes': 'stateAttributesChanged',
},
stateObjChanged: function() {
while (this.$.moreInfo.lastChild) {
this.$.moreInfo.removeChild(this.$.moreInfo.lastChild);
}
var moreInfo = document.createElement("more-info-" + this.stateObj.moreInfoType);
moreInfo.api = this.api;
moreInfo.stateObj = this.stateObj;
this.$.moreInfo.appendChild(moreInfo);
},
stateAttributesChanged: function(oldVal, newVal) {
this.classNames = Object.keys(newVal).map(
function(key) { return "has-" + key; }).join(' ');
},
});
</script>
</polymer-element>

View File

@ -0,0 +1,44 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<polymer-element name="more-info-default" attributes="stateObj">
<template>
<style>
.data-entry {
margin-bottom: 8px;
}
.data-entry:last-child {
margin-bottom: 0;
}
.data {
padding-left: 10px;
text-align: right;
word-break: break-all;
max-width: 200px;
}
</style>
<div layout vertical>
<template repeat="{{key in stateObj.attributes | getKeys}}">
<div layout justified horizontal class='data-entry'>
<div>
{{key}}
</div>
<div class='data'>
{{stateObj.attributes[key]}}
</div>
</div>
</template>
</div>
</template>
<script>
Polymer({
getKeys: function(obj) {
return Object.keys(obj || {});
},
});
</script>
</polymer-element>

View File

@ -0,0 +1,33 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../cards/state-card-content.html">
<polymer-element name="more-info-group" attributes="stateObj api">
<template>
<style>
.child-card {
margin-bottom: 8px;
}
.child-card:last-child {
margin-bottom: 0;
}
</style>
<template repeat="{{states as state}}">
<state-card-content stateObj="{{state}}" api="{{api}}" class='child-card'>
</state-card-content>
</template>
</template>
<script>
Polymer({
observe: {
'stateObj.attributes.entity_id': 'updateStates'
},
updateStates: function() {
this.states = this.api.getStates(this.stateObj.attributes.entity_id);
}
});
</script>
</polymer-element>

View File

@ -0,0 +1,109 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../bower_components/paper-slider/paper-slider.html">
<link rel="import" href="../bower_components/color-picker-element/dist/color-picker.html">
<polymer-element name="more-info-light" attributes="stateObj api">
<template>
<style>
.brightness {
margin-bottom: 8px;
max-height: 0px;
overflow: hidden;
transition: max-height .5s ease-in;
}
.brightness paper-slider::shadow #sliderKnobInner,
.brightness paper-slider::shadow #sliderBar::shadow #activeProgress {
background-color: #039be5;
}
color-picker {
display: block;
width: 350px;
margin: 0 auto;
max-height: 0px;
overflow: hidden;
transition: max-height .5s ease-in .3s;
}
:host-context(.has-brightness) .brightness {
max-height: 500px;
}
:host-context(.has-xy_color) color-picker {
max-height: 500px;
}
</style>
<div>
<div class='brightness'>
<div center horizontal layout>
<div>Brightness</div>
<paper-slider
max="255" flex id='brightness'
on-core-change="{{brightnessSliderChanged}}">
</paper-slider>
</div>
</div>
<color-picker id="colorpicker" width="350" height="200">
</color-picker>
</div>
</template>
<script>
Polymer({
// on-change is unpredictable so using on-core-change this has side effect
// that it fires if changed by brightnessChanged(), thus an ignore boolean.
ignoreNextBrightnessEvent: false,
observe: {
'stateObj.attributes.brightness': 'brightnessChanged',
'stateObj.attributes.xy_color': 'colorChanged'
},
brightnessChanged: function(oldVal, newVal) {
this.ignoreNextBrightnessEvent = true;
this.$.brightness.value = newVal;
},
domReady: function() {
this.$.colorpicker.addEventListener('colorselected', this.colorPicked.bind(this));
},
brightnessSliderChanged: function(ev, details, target) {
if(this.ignoreNextBrightnessEvent) {
this.ignoreNextBrightnessEvent = false;
return;
}
var bri = parseInt(target.value);
if(isNaN(bri)) return;
if(bri === 0) {
this.api.turn_off(this.stateObj.entity_id);
} else {
this.api.call_service("light", "turn_on", {
entity_id: this.stateObj.entity_id,
brightness: bri
});
}
},
colorPicked: function(ev) {
var color = ev.detail.rgb;
this.api.call_service("light", "turn_on", {
entity_id: this.stateObj.entity_id,
rgb_color: [color.r, color.g, color.b]
});
}
});
</script>
</polymer-element>

View File

@ -0,0 +1,62 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<polymer-element name="more-info-sun" attributes="stateObj api">
<template>
<style>
.data-entry {
margin-bottom: 8px;
}
.data-entry:last-child {
margin-bottom: 0;
}
.data {
text-align: right;
}
.time-ago {
color: darkgrey;
margin-top: -2px;
}
</style>
<div layout vertical id='sunData'>
<div layout justified horizontal class='data-entry' id='rising'>
<div>
Rising {{stateObj.attributes.next_rising | relativeHATime}}
</div>
<div class='data'>
{{stateObj.attributes.next_rising | HATimeStripDate}}
</div>
</div>
<div layout justified horizontal class='data-entry' id='setting'>
<div>
Setting {{stateObj.attributes.next_setting | relativeHATime}}
</div>
<div class='data'>
{{stateObj.attributes.next_setting | HATimeStripDate}}
</div>
</div>
</div>
</template>
<script>
Polymer({
stateObjChanged: function() {
var rising = ha.util.parseTime(this.stateObj.attributes.next_rising);
var setting = ha.util.parseTime(this.stateObj.attributes.next_setting);
if(rising > setting) {
this.$.sunData.appendChild(this.$.rising);
} else {
this.$.sunData.appendChild(this.$.setting);
}
}
});
</script>
</polymer-element>

View File

@ -0,0 +1,13 @@
<link rel="import" href="../bower_components/core-icon/core-icon.html">
<link rel="import" href="../bower_components/core-iconset-svg/core-iconset-svg.html">
<core-iconset-svg id="homeassistant" iconSize="100">
<svg><defs>
<g id="thermostat">
<!--
Thermostat icon created by Scott Lewis from the Noun Project
Licensed under CC BY 3.0 - http://creativecommons.org/licenses/by/3.0/us/
-->
<path d="M66.861,60.105V17.453c0-9.06-7.347-16.405-16.408-16.405c-9.06,0-16.404,7.345-16.404,16.405v42.711 c-4.04,4.14-6.533,9.795-6.533,16.035c0,12.684,10.283,22.967,22.967,22.967c12.682,0,22.964-10.283,22.964-22.967 C73.447,69.933,70.933,64.254,66.861,60.105z M60.331,20.38h-13.21v6.536h6.63v6.539h-6.63v6.713h6.63v6.538h-6.63v6.5h6.63v6.536 h-6.63v7.218c-3.775,1.373-6.471,4.993-6.471,9.24h-6.626c0-5.396,2.598-10.182,6.61-13.185V17.446c0-0.038,0.004-0.075,0.004-0.111 l-0.004-0.007c0-5.437,4.411-9.846,9.849-9.846c5.438,0,9.848,4.409,9.848,9.846V20.38z"/></g>
</defs></svg>
</core-iconset-svg>

View File

@ -0,0 +1,60 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../bower_components/core-style/core-style.html">
<polymer-element name="home-assistant-style" noscript>
<template>
<core-style id="ha-dialog">
:host {
font-family: RobotoDraft, 'Helvetica Neue', Helvetica, Arial;
min-width: 350px;
max-width: 700px;
/* First two are from core-transition-bottom */
transition:
transform 0.2s ease-in-out,
opacity 0.2s ease-in,
top .3s,
left .3s !important;
}
:host .sidebar {
margin-left: 30px;
}
@media all and (max-width: 620px) {
:host.two-column {
margin: 0;
width: 100%;
max-height: calc(100% - 64px);
bottom: 0px;
left: 0px;
right: 0px;
}
:host .sidebar {
display: none;
}
}
@media all and (max-width: 464px) {
:host {
margin: 0;
width: 100%;
max-height: calc(100% - 64px);
bottom: 0px;
left: 0px;
right: 0px;
}
}
html /deep/ .ha-form paper-input {
display: block;
}
html /deep/ .ha-form paper-input:first-child {
padding-top: 0;
}
</core-style>
</template>
</polymer-element>

View File

@ -1,108 +0,0 @@
<link rel="import" href="bower_components/polymer/polymer.html">
<link rel="import" href="bower_components/paper-dialog/paper-action-dialog.html">
<link rel="import" href="bower_components/paper-dialog/paper-dialog-transition.html">
<link rel="import" href="bower_components/paper-button/paper-button.html">
<link rel="import" href="bower_components/paper-input/paper-input.html">
<link rel="import" href="bower_components/paper-input/paper-input-decorator.html">
<link rel="import" href="bower_components/paper-input/paper-autogrow-textarea.html">
<link rel="import" href="services-list.html">
<polymer-element name="service-call-dialog" attributes="api">
<template>
<paper-action-dialog id="dialog" heading="Call Service" transition="core-transition-bottom" backdrop="true">
<style>
:host {
font-family: RobotoDraft, 'Helvetica Neue', Helvetica, Arial;
}
paper-input {
display: block;
}
paper-input:first-child {
padding-top: 0;
}
.serviceContainer {
margin-left: 30px;
}
@media all and (max-width: 620px) {
paper-action-dialog {
margin: 0;
width: 100%;
height: calc(100% - 64px);
top: 64px;
}
.serviceContainer {
display: none;
}
}
</style>
<div layout horizontal>
<div>
<paper-input id="inputDomain" label="Domain" floatingLabel="true" autofocus required></paper-input>
<paper-input id="inputService" label="Service" floatingLabel="true" required></paper-input>
<paper-input-decorator
label="Service Data (JSON, optional)"
floatingLabel="true">
<!--
<paper-autogrow-textarea id="inputDataWrapper">
<textarea id="inputData"></textarea>
</paper-autogrow-textarea>
-->
<textarea id="inputData" rows="5"></textarea>
</paper-input-decorator>
</div>
<div class='serviceContainer'>
<b>Available services:</b>
<services-list api={{api}} cbServiceClicked={{serviceSelected}}></event-list>
</div>
</div>
<paper-button dismissive>Cancel</paper-button>
<paper-button affirmative on-click={{clickCallService}}>Call Service</paper-button>
</paper-action-dialog>
</template>
<script>
Polymer({
ready: function() {
// to ensure callback methods work..
this.serviceSelected = this.serviceSelected.bind(this)
},
show: function(domain, service, serviceData) {
this.setService(domain, service);
this.$.inputData.value = serviceData;
// this.$.inputDataWrapper.update();
this.$.dialog.toggle();
},
setService: function(domain, service) {
this.$.inputDomain.value = domain;
this.$.inputService.value = service;
},
serviceSelected: function(domain, service) {
this.setService(domain, service);
},
clickCallService: function() {
var data;
if(this.$.inputData.value != "") {
data = JSON.parse(this.$.inputData.value);
}
this.api.call_service(
this.$.inputDomain.value,
this.$.inputService.value,
data);
}
});
</script>
</polymer-element>

View File

@ -6,6 +6,7 @@
<link rel="import" href="home-assistant-main.html">
<link rel="import" href="home-assistant-api.html">
<link rel="import" href="resources/home-assistant-style.html">
<polymer-element name="splash-login" attributes="auth">
<template>
@ -37,6 +38,7 @@
</style>
<home-assistant-style></home-assistant-style>
<home-assistant-api auth="{{auth}}" id="api"></home-assistant-api>
<div layout horizontal center fit class='login' id="splash">

View File

@ -1,30 +0,0 @@
<script src="bower_components/moment/moment.js"></script>
<link rel="import" href="bower_components/polymer/polymer.html">
<link rel="import" href="bower_components/core-style/core-style.html">
<link rel="import" href="state-info.html">
<polymer-element name="state-card-display"
attributes="stateObj cb_edit"
noscript>
<template>
<core-style ref='state-card'></core-style>
<style>
.state {
text-transform: capitalize;
font-weight: 300;
font-size: 1.3rem;
text-align: right;
}
</style>
<div horizontal justified layout>
<state-info
stateObj="{{stateObj}}"
cb_edit="{{cb_edit}}">
</state-info>
<div class='state'>{{stateObj.stateDisplay}}</div>
</div>
</template>
</polymer-element>

View File

@ -1,117 +0,0 @@
<script src="bower_components/moment/moment.js"></script>
<link rel="import" href="bower_components/polymer/polymer.html">
<link rel="import" href="bower_components/paper-toggle-button/paper-toggle-button.html">
<link rel="import" href="bower_components/core-style/core-style.html">
<link rel="import" href="state-badge.html">
<polymer-element name="state-card-toggle"
attributes="stateObj cb_turn_on, cb_turn_off cb_edit">
<template>
<core-style ref='state-card'></core-style>
<style>
.state {
text-align: right;
}
/* the splash while enabling */
paper-toggle-button::shadow paper-radio-button::shadow #ink[checked] {
color: #0091ea;
}
/* filling of circle when checked */
paper-toggle-button::shadow paper-radio-button::shadow #onRadio {
background-color: #039be5;
}
/* line when checked */
paper-toggle-button::shadow #toggleBar[checked] {
background-color: #039be5;
}
</style>
<div horizontal justified layout>
<state-info
stateObj="{{stateObj}}"
cb_edit="{{cb_edit}}">
</state-info>
<div class='state toggle' self-center flex>
<paper-toggle-button checked="{{toggleChecked}}">
</paper-toggle-button>
</div>
</div>
</template>
<script>
Polymer({
stateObj: {},
cb_turn_on: null,
cb_turn_off: null,
cb_edit: null,
toggleChecked: -1,
observe: {
'stateObj.state': 'stateChanged'
},
lastChangedFromNow: function(lastChanged) {
return moment(lastChanged, "HH:mm:ss DD-MM-YYYY").fromNow();
},
toggleCheckedChanged: function(oldVal, newVal) {
// to filter out init
if(oldVal === -1) {
return;
}
if(newVal && this.stateObj.state == "off") {
this.turn_on();
} else if(!newVal && this.stateObj.state == "on") {
this.turn_off();
}
},
stateChanged: function(oldVal, newVal) {
this.toggleChecked = newVal === "on";
},
turn_on: function() {
// We call stateChanged after a successful call to re-sync the toggle
// with the state. It will be out of sync if our service call did not
// result in the entity to be turned on. Since the state is not changing,
// the resync is not called automatic.
if(this.cb_turn_on) {
this.cb_turn_on(this.stateObj.entity_id, {
success: function() {
this.stateChanged(this.stateObj.state, this.stateObj.state);
}.bind(this)
});
}
},
turn_off: function() {
// We call stateChanged after a successful call to re-sync the toggle
// with the state. It will be out of sync if our service call did not
// result in the entity to be turned on. Since the state is not changing,
// the resync is not called automatic.
if(this.cb_turn_off) {
this.cb_turn_off(this.stateObj.entity_id, {
success: function() {
this.stateChanged(this.stateObj.state, this.stateObj.state);
}.bind(this)
});
}
},
editClicked: function() {
if(this.cb_edit) {
this.cb_edit(this.stateObj.entity_id);
}
},
});
</script>
</polymer-element>

View File

@ -1,74 +0,0 @@
<script src="bower_components/moment/moment.js"></script>
<link rel="import" href="bower_components/polymer/polymer.html">
<link rel="import" href="bower_components/core-tooltip/core-tooltip.html">
<link rel="import" href="bower_components/core-style/core-style.html">
<link rel="import" href="state-badge.html">
<polymer-element name="state-info"
attributes="stateObj cb_edit">
<template>
<style>
state-badge {
float: left;
cursor: pointer;
}
state-badge:hover {
background-color: #039be5;
}
.name {
text-transform: capitalize;
font-weight: 300;
font-size: 1.3rem;
}
.info {
margin-left: 60px;
}
.time-ago {
color: darkgrey;
margin-top: -2px;
}
</style>
<div>
<state-badge
stateObj="{{stateObj}}"
on-click="{{editClicked}}">
</state-badge>
<div class='info'>
<div class='name'>
{{stateObj.entityDisplay}}
</div>
<div class="time-ago">
<core-tooltip label="{{stateObj.last_changed}}" position="bottom">
{{lastChangedFromNow(stateObj.last_changed)}}
</core-tooltip>
</div>
</div>
</div>
</template>
<script>
Polymer({
stateObj: {},
lastChangedFromNow: function(lastChanged) {
return moment(lastChanged, "HH:mm:ss DD-MM-YYYY").fromNow();
},
editClicked: function() {
if(this.cb_edit) {
this.cb_edit(this.stateObj.entity_id);
}
},
});
</script>
</polymer-element>

View File

@ -1,115 +0,0 @@
<link rel="import" href="bower_components/polymer/polymer.html">
<link rel="import" href="bower_components/paper-dialog/paper-action-dialog.html">
<link rel="import" href="bower_components/paper-button/paper-button.html">
<link rel="import" href="bower_components/paper-input/paper-input.html">
<link rel="import" href="bower_components/paper-input/paper-input-decorator.html">
<link rel="import" href="bower_components/paper-input/paper-autogrow-textarea.html">
<link rel="import" href="entity-list.html">
<polymer-element name="state-set-dialog" attributes="api">
<template>
<paper-action-dialog id="dialog" heading="Set State" transition="core-transition-bottom" backdrop="true">
<style>
:host {
font-family: RobotoDraft, 'Helvetica Neue', Helvetica, Arial;
}
paper-input {
display: block;
}
paper-input:first-child {
padding-top: 0;
}
.stateContainer {
margin-left: 30px;
}
@media all and (max-width: 620px) {
paper-action-dialog {
margin: 0;
width: 100%;
height: calc(100% - 64px);
top: 64px;
}
.stateContainer {
display: none;
}
}
</style>
<div layout horizontal>
<div>
<paper-input id="inputEntityID" label="Entity ID" floatingLabel="true" autofocus required></paper-input>
<paper-input id="inputState" label="State" floatingLabel="true" required></paper-input>
<paper-input-decorator
label="State attributes (JSON, optional)"
floatingLabel="true">
<!--
<paper-autogrow-textarea id="inputDataWrapper">
<textarea id="inputData"></textarea>
</paper-autogrow-textarea>
-->
<textarea id="inputData" rows="5"></textarea>
</paper-input-decorator>
</div>
<div class='stateContainer'>
<b>Current entities:</b>
<entity-list api={{api}} cbEntityClicked={{entitySelected}}></entity-list>
</div>
</div>
<paper-button dismissive>Cancel</paper-button>
<paper-button affirmative on-click={{clickSetState}}>Set State</paper-button>
</paper-action-dialog>
</template>
<script>
Polymer({
ready: function() {
// to ensure callback methods work..
this.entitySelected = this.entitySelected.bind(this)
},
show: function(entityId, state, stateData) {
this.setEntityId(entityId);
this.setState(state);
this.setStateData(stateData);
this.$.dialog.toggle();
},
setEntityId: function(entityId) {
this.$.inputEntityID.value = entityId;
},
setState: function(state) {
this.$.inputState.value = state;
},
setStateData: function(stateData) {
var value = stateData ? JSON.stringify(stateData, null, ' ') : "";
this.$.inputData.value = value;
},
entitySelected: function(entityId) {
this.setEntityId(entityId);
var state = this.api.getState(entityId);
this.setState(state.state);
this.setStateData(state.attributes);
},
clickSetState: function() {
this.api.set_state(
this.$.inputEntityID.value,
this.$.inputState.value,
JSON.parse(this.$.inputData.value)
);
}
});
</script>
</polymer-element>

View File

@ -1,117 +0,0 @@
<link rel="import" href="bower_components/polymer/polymer.html">
<link rel="import" href="bower_components/core-style/core-style.html">
<link rel="import" href="state-card-display.html">
<link rel="import" href="state-card-toggle.html">
<polymer-element name="states-cards" attributes="api filter">
<template>
<style>
:host {
display: block;
width: 100%;
}
@media all and (min-width: 764px) {
:host {
padding-bottom: 8px;
}
.state-card {
width: calc(50% - 44px);
margin: 8px 0 0 8px;
}
}
@media all and (min-width: 1100px) {
.state-card {
width: calc(33% - 38px);
}
}
@media all and (min-width: 1450px) {
.state-card {
width: calc(25% - 42px);
}
}
</style>
<core-style id="state-card">
<!-- generic state card CSS -->
:host {
background-color: #fff;
border-radius: 2px;
box-shadow: rgba(0, 0, 0, 0.098) 0px 2px 4px, rgba(0, 0, 0, 0.098) 0px 0px 3px;
transition: all 0.30s ease-out;
position: relative;
background-color: white;
padding: 16px;
width: 100%;
}
</core-style>
<div horizontal layout wrap>
<template id="display">
<state-card-display
class='state-card'
stateObj="{{state}}"
cb_edit={{editCallback}}>
</state-card-display>
</template>
<template id="toggle">
<state-card-toggle
class='state-card'
stateObj="{{state}}"
cb_turn_on="{{api.turn_on}}"
cb_turn_off="{{api.turn_off}}"
cb_edit={{editCallback}}>
</state-card-display>
</template>
<template repeat="{{state in getStates(api.states, filter)}}">
<template bind ref="{{state.cardType}}"></template>
</template>
</div>
</template>
<script>
Polymer({
filter: null,
getStates: function(states, filter) {
if(!states) {
return [];
}
if(!filter) {
// if no filter, return all non-group states
return states.filter(function(state) {
return state.domain != 'group';
});
} else {
// we have a filter, return the parent filter and its children
var filter_state = this.api.getState(this.filter);
var map_states = function(entity_id) {
return this.api.getState(entity_id);
}.bind(this);
return [filter_state].concat(
filter_state.attributes.entity_id.map(map_states));
}
},
ready: function() {
this.editCallback = this.editCallback.bind(this);
},
editCallback: function(entityId) {
this.api.showEditStateDialog(entityId);
},
});
</script>
</polymer-element>

View File

@ -46,7 +46,6 @@ def media_prev_track(hass):
hass.services.call(DOMAIN, SERVICE_MEDIA_PREV_TRACK)
# pylint: disable=unused-argument
def setup(hass, config):
""" Listen for keyboard events. """
try:

View File

@ -52,20 +52,20 @@ import logging
import os
import csv
from homeassistant.loader import get_component
import homeassistant.util as util
from homeassistant.const import (
STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID)
from homeassistant.helpers import (
extract_entity_ids, platform_devices_from_config)
from homeassistant.components import group
generate_entity_id, extract_entity_ids, config_per_platform)
from homeassistant.components import group, discovery, wink
DOMAIN = "light"
DEPENDENCIES = []
GROUP_NAME_ALL_LIGHTS = 'all_lights'
ENTITY_ID_ALL_LIGHTS = group.ENTITY_ID_FORMAT.format(
GROUP_NAME_ALL_LIGHTS)
GROUP_NAME_ALL_LIGHTS = 'all lights'
ENTITY_ID_ALL_LIGHTS = group.ENTITY_ID_FORMAT.format('all_lights')
ENTITY_ID_FORMAT = DOMAIN + ".{}"
@ -87,9 +87,14 @@ ATTR_FLASH = "flash"
FLASH_SHORT = "short"
FLASH_LONG = "long"
LIGHT_PROFILES_FILE = "light_profiles.csv"
# Maps discovered services to their platforms
DISCOVERY_PLATFORMS = {
wink.DISCOVER_LIGHTS: 'wink',
discovery.services.PHILIPS_HUE: 'hue',
}
_LOGGER = logging.getLogger(__name__)
@ -163,40 +168,51 @@ def setup(hass, config):
return False
lights = platform_devices_from_config(config, DOMAIN, hass, _LOGGER)
# Dict to track entity_id -> lights
lights = {}
if not lights:
return False
# Track all lights in a group
light_group = group.Group(hass, GROUP_NAME_ALL_LIGHTS, user_defined=False)
ent_to_light = {}
def add_lights(new_lights):
""" Add lights to the component to track. """
for light in new_lights:
if light is not None and light not in lights.values():
light.entity_id = generate_entity_id(
ENTITY_ID_FORMAT, light.name, lights.keys())
no_name_count = 1
lights[light.entity_id] = light
for light in lights:
name = light.get_name()
light.update_ha_state(hass)
if name is None:
name = "Light #{}".format(no_name_count)
no_name_count += 1
light_group.update_tracked_entity_ids(lights.keys())
entity_id = util.ensure_unique_string(
ENTITY_ID_FORMAT.format(util.slugify(name)),
ent_to_light.keys())
for p_type, p_config in config_per_platform(config, DOMAIN, _LOGGER):
platform = get_component(ENTITY_ID_FORMAT.format(p_type))
light.entity_id = entity_id
ent_to_light[entity_id] = light
if platform is None:
_LOGGER.error("Unknown type specified: %s", p_type)
platform.setup_platform(hass, p_config, add_lights)
# pylint: disable=unused-argument
def update_lights_state(now):
""" Update the states of all the lights. """
for light in lights:
light.update_ha_state(hass)
if lights:
_LOGGER.info("Updating light states")
for light in lights.values():
light.update_ha_state(hass, True)
update_lights_state(None)
# Track all lights in a group
group.setup_group(
hass, GROUP_NAME_ALL_LIGHTS, ent_to_light.keys(), False)
def light_discovered(service, info):
""" Called when a light is discovered. """
platform = get_component(
ENTITY_ID_FORMAT.format(DISCOVERY_PLATFORMS[service]))
platform.setup_platform(hass, {}, add_lights, info)
discovery.listen(hass, DISCOVERY_PLATFORMS.keys(), light_discovered)
def handle_light_service(service):
""" Hande a turn light on or off service call. """
@ -204,12 +220,12 @@ def setup(hass, config):
dat = service.data
# Convert the entity ids to valid light ids
lights = [ent_to_light[entity_id] for entity_id
in extract_entity_ids(hass, service)
if entity_id in ent_to_light]
target_lights = [lights[entity_id] for entity_id
in extract_entity_ids(hass, service)
if entity_id in lights]
if not lights:
lights = list(ent_to_light.values())
if not target_lights:
target_lights = lights.values()
params = {}
@ -219,7 +235,7 @@ def setup(hass, config):
params[ATTR_TRANSITION] = transition
if service.service == SERVICE_TURN_OFF:
for light in lights:
for light in target_lights:
# pylint: disable=star-args
light.turn_off(**params)
@ -277,11 +293,11 @@ def setup(hass, config):
elif dat[ATTR_FLASH] == FLASH_LONG:
params[ATTR_FLASH] = FLASH_LONG
for light in lights:
for light in target_lights:
# pylint: disable=star-args
light.turn_on(**params)
for light in lights:
for light in target_lights:
light.update_ha_state(hass, True)
# Update light state every 30 seconds

View File

@ -2,7 +2,9 @@
import logging
import socket
from datetime import timedelta
from urllib.parse import urlparse
from homeassistant.loader import get_component
import homeassistant.util as util
from homeassistant.helpers import ToggleDevice
from homeassistant.const import ATTR_FRIENDLY_NAME, CONF_HOST
@ -16,27 +18,59 @@ MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1)
PHUE_CONFIG_FILE = "phue.conf"
def get_devices(hass, config):
# Map ip to request id for configuring
_CONFIGURING = {}
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
""" Gets the Hue lights. """
logger = logging.getLogger(__name__)
try:
import phue
# pylint: disable=unused-variable
import phue # noqa
except ImportError:
logger.exception("Error while importing dependency phue.")
_LOGGER.exception("Error while importing dependency phue.")
return []
return
host = config.get(CONF_HOST, None)
if discovery_info is not None:
host = urlparse(discovery_info).hostname
else:
host = config.get(CONF_HOST, None)
# Only act if we are not already configuring this host
if host in _CONFIGURING:
return
setup_bridge(host, hass, add_devices_callback)
def setup_bridge(host, hass, add_devices_callback):
""" Setup a phue bridge based on host parameter. """
import phue
try:
bridge = phue.Bridge(
host, config_file_path=hass.get_config_path(PHUE_CONFIG_FILE))
except socket.error: # Error connecting using Phue
logger.exception((
"Error while connecting to the bridge. "
"Did you follow the instructions to set it up?"))
except ConnectionRefusedError: # Wrong host was given
_LOGGER.exception("Error connecting to the Hue bridge at %s", host)
return []
return
except phue.PhueRegistrationException:
_LOGGER.warning("Connected to Hue at %s but not registered.", host)
request_configuration(host, hass, add_devices_callback)
return
# If we came here and configuring this host, mark as done
if host in _CONFIGURING:
request_id = _CONFIGURING.pop(host)
configurator = get_component('configurator')
configurator.request_done(request_id)
lights = {}
@ -47,25 +81,53 @@ def get_devices(hass, config):
api = bridge.get_api()
except socket.error:
# socket.error when we cannot reach Hue
logger.exception("Cannot reach the bridge")
_LOGGER.exception("Cannot reach the bridge")
return
api_states = api.get('lights')
if not isinstance(api_states, dict):
logger.error("Got unexpected result from Hue API")
_LOGGER.error("Got unexpected result from Hue API")
return
new_lights = []
for light_id, info in api_states.items():
if light_id not in lights:
lights[light_id] = HueLight(int(light_id), info,
bridge, update_lights)
new_lights.append(lights[light_id])
else:
lights[light_id].info = info
if new_lights:
add_devices_callback(new_lights)
update_lights()
return list(lights.values())
def request_configuration(host, hass, add_devices_callback):
""" Request configuration steps from the user. """
configurator = get_component('configurator')
# We got an error if this method is called while we are configuring
if host in _CONFIGURING:
configurator.notify_errors(
_CONFIGURING[host], "Failed to register, please try again.")
return
def hue_configuration_callback(data):
""" Actions to do when our configuration callback is called. """
setup_bridge(host, hass, add_devices_callback)
_CONFIGURING[host] = configurator.request_config(
hass, "Philips Hue", hue_configuration_callback,
description=("Press the button on the bridge to register Philips Hue "
"with Home Assistant."),
description_image="/static/images/config_philips_hue.jpg",
submit_caption="I have pressed the button"
)
class HueLight(ToggleDevice):
@ -77,9 +139,36 @@ class HueLight(ToggleDevice):
self.bridge = bridge
self.update_lights = update_lights
def get_name(self):
@property
def unique_id(self):
""" Returns the id of this Hue light """
return "{}.{}".format(
self.__class__, self.info.get('uniqueid', self.name))
@property
def name(self):
""" Get the mame of the Hue light. """
return self.info['name']
return self.info.get('name', 'No name')
@property
def state_attributes(self):
""" Returns optional state attributes. """
attr = {
ATTR_FRIENDLY_NAME: self.name
}
if self.is_on:
attr[ATTR_BRIGHTNESS] = self.info['state']['bri']
attr[ATTR_XY_COLOR] = self.info['state']['xy']
return attr
@property
def is_on(self):
""" True if device is on. """
self.update_lights()
return self.info['state']['reachable'] and self.info['state']['on']
def turn_on(self, **kwargs):
""" Turn the specified or all lights on. """
@ -118,24 +207,6 @@ class HueLight(ToggleDevice):
self.bridge.set_light(self.light_id, command)
def is_on(self):
""" True if device is on. """
self.update_lights()
return self.info['state']['reachable'] and self.info['state']['on']
def get_state_attributes(self):
""" Returns optional state attributes. """
attr = {
ATTR_FRIENDLY_NAME: self.get_name()
}
if self.is_on():
attr[ATTR_BRIGHTNESS] = self.info['state']['bri']
attr[ATTR_XY_COLOR] = self.info['state']['xy']
return attr
def update(self):
""" Synchronize state with bridge. """
self.update_lights(no_throttle=True)

View File

@ -0,0 +1,53 @@
""" Support for Hue lights. """
import logging
# pylint: disable=no-name-in-module, import-error
import homeassistant.external.wink.pywink as pywink
from homeassistant.components.light import ATTR_BRIGHTNESS
from homeassistant.components.wink import WinkToggleDevice
from homeassistant.const import CONF_ACCESS_TOKEN
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
""" Find and return Wink lights. """
token = config.get(CONF_ACCESS_TOKEN)
if not pywink.is_token_set() and token is None:
logging.getLogger(__name__).error(
"Missing wink access_token - "
"get one at https://winkbearertoken.appspot.com/")
return
elif token is not None:
pywink.set_bearer_token(token)
add_devices_callback(
WinkLight(light) for light in pywink.get_bulbs())
class WinkLight(WinkToggleDevice):
""" Represents a Wink light """
# pylint: disable=too-few-public-methods
def turn_on(self, **kwargs):
""" Turns the switch on. """
brightness = kwargs.get(ATTR_BRIGHTNESS)
if brightness is not None:
self.wink.setState(True, brightness / 255)
else:
self.wink.setState(True)
@property
def state_attributes(self):
attr = super().state_attributes
if self.is_on:
brightness = self.wink.brightness()
if brightness is not None:
attr[ATTR_BRIGHTNESS] = int(brightness * 255)
return attr

View File

@ -0,0 +1,83 @@
"""
homeassistant.components.notify
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Provides functionality to notify people.
"""
import logging
from homeassistant.loader import get_component
from homeassistant.helpers import validate_config
from homeassistant.const import CONF_PLATFORM
DOMAIN = "notify"
DEPENDENCIES = []
# Title of notification
ATTR_TITLE = "title"
ATTR_TITLE_DEFAULT = "Home Assistant"
# Text to notify user of
ATTR_MESSAGE = "message"
SERVICE_NOTIFY = "notify"
_LOGGER = logging.getLogger(__name__)
def send_message(hass, message):
""" Send a notification message. """
hass.services.call(DOMAIN, SERVICE_NOTIFY, {ATTR_MESSAGE: message})
def setup(hass, config):
""" Sets up notify services. """
if not validate_config(config, {DOMAIN: [CONF_PLATFORM]}, _LOGGER):
return False
platform = config[DOMAIN].get(CONF_PLATFORM)
notify_implementation = get_component(
'notify.{}'.format(platform))
if notify_implementation is None:
_LOGGER.error("Unknown notification service specified.")
return False
notify_service = notify_implementation.get_service(hass, config)
if notify_service is None:
_LOGGER.error("Failed to initialize notification service %s",
platform)
return False
def notify_message(call):
""" Handle sending notification message service calls. """
message = call.data.get(ATTR_MESSAGE)
if message is None:
return
title = call.data.get(ATTR_TITLE, ATTR_TITLE_DEFAULT)
notify_service.send_message(message, title=title)
hass.services.register(DOMAIN, SERVICE_NOTIFY, notify_message)
return True
# pylint: disable=too-few-public-methods
class BaseNotificationService(object):
""" Provides an ABC for notifcation services. """
def send_message(self, message, **kwargs):
"""
Send a message.
kwargs can contain ATTR_TITLE to specify a title.
"""
raise NotImplementedError

View File

@ -0,0 +1,56 @@
"""
PushBullet platform for notify component.
"""
import logging
from homeassistant.helpers import validate_config
from homeassistant.components.notify import (
DOMAIN, ATTR_TITLE, BaseNotificationService)
from homeassistant.const import CONF_API_KEY
_LOGGER = logging.getLogger(__name__)
def get_service(hass, config):
""" Get the pushbullet notification service. """
if not validate_config(config,
{DOMAIN: [CONF_API_KEY]},
_LOGGER):
return None
try:
# pylint: disable=unused-variable
from pushbullet import PushBullet, InvalidKeyError # noqa
except ImportError:
_LOGGER.exception(
"Unable to import pushbullet. "
"Did you maybe not install the 'pushbullet.py' package?")
return None
try:
return PushBulletNotificationService(config[DOMAIN][CONF_API_KEY])
except InvalidKeyError:
_LOGGER.error(
"Wrong API key supplied. "
"Get it at https://www.pushbullet.com/account")
# pylint: disable=too-few-public-methods
class PushBulletNotificationService(BaseNotificationService):
""" Implements notification service for Pushbullet. """
def __init__(self, api_key):
from pushbullet import PushBullet
self.pushbullet = PushBullet(api_key)
def send_message(self, message="", **kwargs):
""" Send a message to a user. """
title = kwargs.get(ATTR_TITLE)
self.pushbullet.push_note(title, message)

View File

@ -30,7 +30,6 @@ def setup(hass, config):
entities = {ENTITY_ID_FORMAT.format(util.slugify(pname)): pstring
for pname, pstring in config[DOMAIN].items()}
# pylint: disable=unused-argument
def update_process_states(time):
""" Check ps for currently running processes and update states. """
with os.popen(PS_STRING, 'r') as psfile:

View File

@ -0,0 +1,89 @@
"""
homeassistant.components.sensor
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Component to interface with various sensors that can be monitored.
"""
import logging
from datetime import timedelta
from homeassistant.loader import get_component
import homeassistant.util as util
from homeassistant.const import (
STATE_OPEN)
from homeassistant.helpers import (
platform_devices_from_config)
from homeassistant.components import group, discovery, wink
DOMAIN = 'sensor'
DEPENDENCIES = []
GROUP_NAME_ALL_SENSORS = 'all_sensors'
ENTITY_ID_ALL_SENSORS = group.ENTITY_ID_FORMAT.format(
GROUP_NAME_ALL_SENSORS)
ENTITY_ID_FORMAT = DOMAIN + '.{}'
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=1)
# Maps discovered services to their platforms
DISCOVERY_PLATFORMS = {
wink.DISCOVER_SENSORS: 'wink',
}
_LOGGER = logging.getLogger(__name__)
def is_on(hass, entity_id=None):
""" Returns if the sensor is open based on the statemachine. """
entity_id = entity_id or ENTITY_ID_ALL_SENSORS
return hass.states.is_state(entity_id, STATE_OPEN)
def setup(hass, config):
""" Track states and offer events for sensors. """
logger = logging.getLogger(__name__)
sensors = platform_devices_from_config(
config, DOMAIN, hass, ENTITY_ID_FORMAT, logger)
@util.Throttle(MIN_TIME_BETWEEN_SCANS)
def update_sensor_states(now):
""" Update states of all sensors. """
if sensors:
logger.info("Updating sensor states")
for sensor in sensors.values():
sensor.update_ha_state(hass, True)
update_sensor_states(None)
# Track all sensors in a group
sensor_group = group.Group(
hass, GROUP_NAME_ALL_SENSORS, sensors.keys(), False)
def sensor_discovered(service, info):
""" Called when a sensor is discovered. """
platform = get_component("{}.{}".format(
DOMAIN, DISCOVERY_PLATFORMS[service]))
discovered = platform.devices_discovered(hass, config, info)
for sensor in discovered:
if sensor is not None and sensor not in sensors.values():
sensor.entity_id = util.ensure_unique_string(
ENTITY_ID_FORMAT.format(util.slugify(sensor.name)),
sensors.keys())
sensors[sensor.entity_id] = sensor
sensor.update_ha_state(hass)
sensor_group.update_tracked_entity_ids(sensors.keys())
discovery.listen(hass, DISCOVERY_PLATFORMS.keys(), sensor_discovered)
# Fire every 3 seconds
hass.track_time_change(update_sensor_states, seconds=range(0, 60, 3))
return True

View File

@ -0,0 +1,33 @@
""" Support for Wink sensors. """
import logging
# pylint: disable=no-name-in-module, import-error
import homeassistant.external.wink.pywink as pywink
from homeassistant.components.wink import WinkSensorDevice
from homeassistant.const import CONF_ACCESS_TOKEN
def get_devices(hass, config):
""" Find and return Wink sensors. """
token = config.get(CONF_ACCESS_TOKEN)
if token is None:
logging.getLogger(__name__).error(
"Missing wink access_token - "
"get one at https://winkbearertoken.appspot.com/")
return []
pywink.set_bearer_token(token)
return get_sensors()
def devices_discovered(hass, config, info):
""" Called when a device is discovered. """
return get_sensors()
def get_sensors():
""" Returns the Wink sensors. """
return [WinkSensorDevice(sensor) for sensor in pywink.get_sensors()]

View File

@ -34,6 +34,7 @@ def setup(hass, config):
device_tracker = loader.get_component('device_tracker')
light = loader.get_component('light')
notify = loader.get_component('notify')
light_ids = []
@ -67,13 +68,16 @@ def setup(hass, config):
hass, unknown_light_id,
flash=light.FLASH_LONG, rgb_color=[255, 0, 0])
# Send a message to the user
notify.send_message(
hass, "The lights just got turned on while no one was home.")
# Setup services to test the effect
hass.services.register(
DOMAIN, SERVICE_TEST_KNOWN_ALARM, lambda call: known_alarm())
hass.services.register(
DOMAIN, SERVICE_TEST_UNKNOWN_ALARM, lambda call: unknown_alarm())
# pylint: disable=unused-argument
def unknown_alarm_if_lights_on(entity_id, old_state, new_state):
""" Called when a light has been turned on. """
if not device_tracker.is_on(hass):
@ -83,7 +87,6 @@ def setup(hass, config):
light.ENTITY_ID_ALL_LIGHTS,
unknown_alarm_if_lights_on, STATE_OFF, STATE_ON)
# pylint: disable=unused-argument
def ring_known_alarm(entity_id, old_state, new_state):
""" Called when a known person comes home. """
if light.is_on(hass, known_light_id):

View File

@ -6,19 +6,19 @@ Component to interface with various switches that can be controlled remotely.
import logging
from datetime import timedelta
from homeassistant.loader import get_component
import homeassistant.util as util
from homeassistant.const import (
STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID)
from homeassistant.helpers import (
extract_entity_ids, platform_devices_from_config)
from homeassistant.components import group
generate_entity_id, extract_entity_ids, platform_devices_from_config)
from homeassistant.components import group, discovery, wink
DOMAIN = 'switch'
DEPENDENCIES = []
GROUP_NAME_ALL_SWITCHES = 'all_switches'
ENTITY_ID_ALL_SWITCHES = group.ENTITY_ID_FORMAT.format(
GROUP_NAME_ALL_SWITCHES)
GROUP_NAME_ALL_SWITCHES = 'all switches'
ENTITY_ID_ALL_SWITCHES = group.ENTITY_ID_FORMAT.format('all_switches')
ENTITY_ID_FORMAT = DOMAIN + '.{}'
@ -27,6 +27,12 @@ ATTR_CURRENT_POWER_MWH = "current_power_mwh"
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
# Maps discovered services to their platforms
DISCOVERY_PLATFORMS = {
discovery.services.BELKIN_WEMO: 'wemo',
wink.DISCOVER_SWITCHES: 'wink',
}
_LOGGER = logging.getLogger(__name__)
@ -55,52 +61,54 @@ def setup(hass, config):
""" Track states and offer events for switches. """
logger = logging.getLogger(__name__)
switches = platform_devices_from_config(config, DOMAIN, hass, logger)
switches = platform_devices_from_config(
config, DOMAIN, hass, ENTITY_ID_FORMAT, logger)
if not switches:
return False
# Setup a dict mapping entity IDs to devices
ent_to_switch = {}
no_name_count = 1
for switch in switches:
name = switch.get_name()
if name is None:
name = "Switch #{}".format(no_name_count)
no_name_count += 1
entity_id = util.ensure_unique_string(
ENTITY_ID_FORMAT.format(util.slugify(name)),
ent_to_switch.keys())
switch.entity_id = entity_id
ent_to_switch[entity_id] = switch
# pylint: disable=unused-argument
@util.Throttle(MIN_TIME_BETWEEN_SCANS)
def update_states(now):
""" Update states of all switches. """
if switches:
logger.info("Updating switch states")
logger.info("Updating switch states")
for switch in switches:
switch.update_ha_state(hass)
for switch in switches.values():
switch.update_ha_state(hass, True)
update_states(None)
# Track all switches in a group
switch_group = group.Group(
hass, GROUP_NAME_ALL_SWITCHES, switches.keys(), False)
def switch_discovered(service, info):
""" Called when a switch is discovered. """
platform = get_component("{}.{}".format(
DOMAIN, DISCOVERY_PLATFORMS[service]))
discovered = platform.devices_discovered(hass, config, info)
for switch in discovered:
if switch is not None and switch not in switches.values():
switch.entity_id = generate_entity_id(
ENTITY_ID_FORMAT, switch.name, switches.keys())
switches[switch.entity_id] = switch
switch.update_ha_state(hass)
switch_group.update_tracked_entity_ids(switches.keys())
discovery.listen(hass, DISCOVERY_PLATFORMS.keys(), switch_discovered)
def handle_switch_service(service):
""" Handles calls to the switch services. """
devices = [ent_to_switch[entity_id] for entity_id
in extract_entity_ids(hass, service)
if entity_id in ent_to_switch]
target_switches = [switches[entity_id] for entity_id
in extract_entity_ids(hass, service)
if entity_id in switches]
if not devices:
devices = switches
if not target_switches:
target_switches = switches.values()
for switch in devices:
for switch in target_switches:
if service.service == SERVICE_TURN_ON:
switch.turn_on()
else:
@ -108,10 +116,6 @@ def setup(hass, config):
switch.update_ha_state(hass)
# Track all switches in a group
group.setup_group(hass, GROUP_NAME_ALL_SWITCHES,
ent_to_switch.keys(), False)
# Update state every 30 seconds
hass.track_time_change(update_states, second=[0, 30])

View File

@ -11,7 +11,6 @@ except ImportError:
pass
# pylint: disable=unused-argument
def get_devices(hass, config):
""" Find and return Tellstick switches. """
try:
@ -36,20 +35,17 @@ class TellstickSwitch(ToggleDevice):
self.tellstick = tellstick
self.state_attr = {ATTR_FRIENDLY_NAME: tellstick.name}
def get_name(self):
@property
def name(self):
""" Returns the name of the switch if any. """
return self.tellstick.name
# pylint: disable=unused-argument
def turn_on(self, **kwargs):
""" Turns the switch on. """
self.tellstick.turn_on()
# pylint: disable=unused-argument
def turn_off(self, **kwargs):
""" Turns the switch off. """
self.tellstick.turn_off()
@property
def state_attributes(self):
""" Returns optional state attributes. """
return self.state_attr
@property
def is_on(self):
""" True if switch is on. """
last_command = self.tellstick.last_sent_command(
@ -57,6 +53,10 @@ class TellstickSwitch(ToggleDevice):
return last_command == tc_constants.TELLSTICK_TURNON
def get_state_attributes(self):
""" Returns optional state attributes. """
return self.state_attr
def turn_on(self, **kwargs):
""" Turns the switch on. """
self.tellstick.turn_on()
def turn_off(self, **kwargs):
""" Turns the switch off. """
self.tellstick.turn_off()

View File

@ -2,38 +2,55 @@
import logging
from homeassistant.helpers import ToggleDevice
from homeassistant.const import ATTR_FRIENDLY_NAME, CONF_HOSTS
from homeassistant.const import ATTR_FRIENDLY_NAME
from homeassistant.components.switch import (
ATTR_TODAY_MWH, ATTR_CURRENT_POWER_MWH)
# pylint: disable=unused-argument
def get_devices(hass, config):
""" Find and return WeMo switches. """
pywemo, _ = get_pywemo()
if pywemo is None:
return []
logging.getLogger(__name__).info("Scanning for WeMo devices")
switches = pywemo.discover_devices()
# Filter out the switches and wrap in WemoSwitch object
return [WemoSwitch(switch) for switch in switches
if isinstance(switch, pywemo.Switch)]
def devices_discovered(hass, config, info):
""" Called when a device is discovered. """
_, discovery = get_pywemo()
if discovery is None:
return []
device = discovery.device_from_description(info)
return [] if device is None else [WemoSwitch(device)]
def get_pywemo():
""" Tries to import PyWemo. """
try:
# Pylint does not play nice if not every folders has an __init__.py
# pylint: disable=no-name-in-module, import-error
import homeassistant.external.pywemo.pywemo as pywemo
import homeassistant.external.pywemo.pywemo.discovery as discovery
return pywemo, discovery
except ImportError:
logging.getLogger(__name__).exception((
"Failed to import pywemo. "
"Did you maybe not run `git submodule init` "
"and `git submodule update`?"))
return []
if CONF_HOSTS in config:
switches = (pywemo.device_from_host(host) for host
in config[CONF_HOSTS].split(","))
else:
logging.getLogger(__name__).info("Scanning for WeMo devices")
switches = pywemo.discover_devices()
# Filter out the switches and wrap in WemoSwitch object
return [WemoSwitch(switch) for switch in switches
if isinstance(switch, pywemo.Switch)]
return None, None
class WemoSwitch(ToggleDevice):
@ -41,23 +58,18 @@ class WemoSwitch(ToggleDevice):
def __init__(self, wemo):
self.wemo = wemo
def get_name(self):
@property
def unique_id(self):
""" Returns the id of this WeMo switch """
return "{}.{}".format(self.__class__, self.wemo.serialnumber)
@property
def name(self):
""" Returns the name of the switch if any. """
return self.wemo.name
def turn_on(self, **kwargs):
""" Turns the switch on. """
self.wemo.on()
def turn_off(self):
""" Turns the switch off. """
self.wemo.off()
def is_on(self):
""" True if switch is on. """
return self.wemo.get_state(True)
def get_state_attributes(self):
@property
def state_attributes(self):
""" Returns optional state attributes. """
if self.wemo.model.startswith('Belkin Insight'):
cur_info = self.wemo.insight_params
@ -69,3 +81,20 @@ class WemoSwitch(ToggleDevice):
}
else:
return {ATTR_FRIENDLY_NAME: self.wemo.name}
@property
def is_on(self):
""" True if switch is on. """
return self.wemo.get_state()
def turn_on(self, **kwargs):
""" Turns the switch on. """
self.wemo.on()
def turn_off(self):
""" Turns the switch off. """
self.wemo.off()
def update(self):
""" Update Wemo state. """
self.wemo.get_state(True)

View File

@ -0,0 +1,33 @@
""" Support for WeMo switchces. """
import logging
# pylint: disable=no-name-in-module, import-error
import homeassistant.external.wink.pywink as pywink
from homeassistant.components.wink import WinkToggleDevice
from homeassistant.const import CONF_ACCESS_TOKEN
def get_devices(hass, config):
""" Find and return Wink switches. """
token = config.get(CONF_ACCESS_TOKEN)
if token is None:
logging.getLogger(__name__).error(
"Missing wink access_token - "
"get one at https://winkbearertoken.appspot.com/")
return []
pywink.set_bearer_token(token)
return get_switches()
def devices_discovered(hass, config, info):
""" Called when a device is discovered. """
return get_switches()
def get_switches():
""" Returns the Wink switches. """
return [WinkToggleDevice(switch) for switch in pywink.get_switches()]

View File

@ -129,7 +129,6 @@ def setup(hass, config):
sensor.has_value(datatype):
update_sensor_value_state(sensor_name, sensor.value(datatype))
# pylint: disable=unused-argument
def update_sensors_state(time):
""" Update the state of all sensors """
for sensor in sensors:

View File

@ -0,0 +1,193 @@
"""
homeassistant.components.thermostat
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Provides functionality to interact with thermostats.
"""
import logging
from datetime import timedelta
from homeassistant.helpers import (
extract_entity_ids, platform_devices_from_config)
import homeassistant.util as util
from homeassistant.helpers import Device
from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_TEMPERATURE, ATTR_UNIT_OF_MEASUREMENT,
STATE_ON, STATE_OFF)
DOMAIN = "thermostat"
ENTITY_ID_FORMAT = DOMAIN + ".{}"
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
DEPENDENCIES = []
SERVICE_TURN_AWAY_MODE_ON = "turn_away_mode_on"
SERVICE_TURN_AWAY_MODE_OFF = "turn_away_mode_off"
SERVICE_SET_TEMPERATURE = "set_temperature"
ATTR_CURRENT_TEMPERATURE = "current_temperature"
ATTR_AWAY_MODE = "away_mode"
_LOGGER = logging.getLogger(__name__)
def turn_away_mode_on(hass, entity_id=None):
""" Turn all or specified thermostat away mode on. """
data = {ATTR_ENTITY_ID: entity_id} if entity_id else None
hass.services.call(DOMAIN, SERVICE_TURN_AWAY_MODE_ON, data)
def turn_away_mode_off(hass, entity_id=None):
""" Turn all or specified thermostat away mode off. """
data = {ATTR_ENTITY_ID: entity_id} if entity_id else None
hass.services.call(DOMAIN, SERVICE_TURN_AWAY_MODE_OFF, data)
def set_temperature(hass, temperature, entity_id=None):
""" Set new target temperature. """
data = {ATTR_TEMPERATURE: temperature}
if entity_id is not None:
data[ATTR_ENTITY_ID] = entity_id
hass.services.call(DOMAIN, SERVICE_SET_TEMPERATURE, data)
def setup(hass, config):
""" Setup thermostats. """
logger = logging.getLogger(__name__)
thermostats = platform_devices_from_config(
config, DOMAIN, hass, ENTITY_ID_FORMAT, _LOGGER)
if not thermostats:
return False
@util.Throttle(MIN_TIME_BETWEEN_SCANS)
def update_state(now):
""" Update thermostat state. """
logger.info("Updating nest state")
for thermostat in thermostats.values():
thermostat.update_ha_state(hass, True)
# Update state every minute
hass.track_time_change(update_state, second=[0])
update_state(None)
def thermostat_service(service):
""" Handles calls to the services. """
# Convert the entity ids to valid light ids
target_thermostats = [thermostats[entity_id] for entity_id
in extract_entity_ids(hass, service)
if entity_id in thermostats]
if not target_thermostats:
target_thermostats = thermostats.values()
if service.service == SERVICE_TURN_AWAY_MODE_ON:
for thermostat in target_thermostats:
thermostat.turn_away_mode_on()
elif service.service == SERVICE_TURN_AWAY_MODE_OFF:
for thermostat in target_thermostats:
thermostat.turn_away_mode_off()
elif service.service == SERVICE_SET_TEMPERATURE:
temperature = util.convert(
service.data.get(ATTR_TEMPERATURE), float)
if temperature is None:
return
for thermostat in target_thermostats:
thermostat.nest.set_temperature(temperature)
for thermostat in target_thermostats:
thermostat.update_ha_state(hass, True)
hass.services.register(
DOMAIN, SERVICE_TURN_AWAY_MODE_OFF, thermostat_service)
hass.services.register(
DOMAIN, SERVICE_TURN_AWAY_MODE_ON, thermostat_service)
hass.services.register(
DOMAIN, SERVICE_SET_TEMPERATURE, thermostat_service)
return True
class ThermostatDevice(Device):
""" Represents a thermostat within Home Assistant. """
# pylint: disable=no-self-use
@property
def state(self):
""" Returns the current state. """
return self.target_temperature
@property
def unit_of_measurement(self):
""" Returns the unit of measurement. """
return ""
@property
def device_state_attributes(self):
""" Returns device specific state attributes. """
return None
@property
def state_attributes(self):
""" Returns optional state attributes. """
data = {
ATTR_UNIT_OF_MEASUREMENT: self.unit_of_measurement,
ATTR_CURRENT_TEMPERATURE: self.current_temperature
}
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
device_attr = self.device_state_attributes
if device_attr is not None:
data.update(device_attr)
return data
@property
def current_temperature(self):
""" Returns the current temperature. """
raise NotImplementedError
@property
def target_temperature(self):
""" Returns the temperature we try to reach. """
raise NotImplementedError
@property
def is_away_mode_on(self):
"""
Returns if away mode is on.
Return None if no away mode available.
"""
return None
def set_temperate(self, temperature):
""" Set new target temperature. """
pass
def turn_away_mode_on(self):
""" Turns away mode on. """
pass
def turn_away_mode_off(self):
""" Turns away mode off. """
pass

View File

@ -0,0 +1,96 @@
"""
Adds support for Nest thermostats.
"""
import logging
from homeassistant.components.thermostat import ThermostatDevice
from homeassistant.const import (CONF_USERNAME, CONF_PASSWORD, TEMP_CELCIUS)
def get_devices(hass, config):
""" Gets Nest thermostats. """
logger = logging.getLogger(__name__)
username = config.get(CONF_USERNAME)
password = config.get(CONF_PASSWORD)
if username is None or password is None:
logger.error("Missing required configuration items %s or %s",
CONF_USERNAME, CONF_PASSWORD)
return []
try:
import nest
except ImportError:
logger.exception(
"Error while importing dependency nest. "
"Did you maybe not install the python-nest dependency?")
return []
napi = nest.Nest(username, password)
return [
NestThermostat(structure, device)
for structure in napi.structures
for device in structure.devices]
class NestThermostat(ThermostatDevice):
""" Represents a Nest thermostat within Home Assistant. """
def __init__(self, structure, device):
self.structure = structure
self.device = device
@property
def name(self):
""" Returns the name of the nest, if any. """
return self.device.name
@property
def unit_of_measurement(self):
""" Returns the unit of measurement. """
return TEMP_CELCIUS
@property
def device_state_attributes(self):
""" Returns device specific state attributes. """
# Move these to Thermostat Device and make them global
return {
"humidity": self.device.humidity,
"target_humidity": self.device.target_humidity,
"fan": self.device.fan,
"mode": self.device.mode
}
@property
def current_temperature(self):
""" Returns the current temperature. """
return round(self.device.temperature, 1)
@property
def target_temperature(self):
""" Returns the temperature we try to reach. """
return round(self.device.target, 1)
@property
def is_away_mode_on(self):
""" Returns if away mode is on. """
return self.structure.away
def set_temperature(self, temperature):
""" Set new target temperature """
self.device.target = temperature
def turn_away_mode_on(self):
""" Turns away on. """
self.structure.away = True
def turn_away_mode_off(self):
""" Turns away off. """
self.structure.away = False
def update(self):
""" Python-nest has its own mechanism for staying up to date. """
pass

View File

@ -0,0 +1,132 @@
"""
Connects to a Wink hub and loads relevant components to control its devices.
"""
import logging
# pylint: disable=no-name-in-module, import-error
import homeassistant.external.wink.pywink as pywink
from homeassistant import bootstrap
from homeassistant.loader import get_component
from homeassistant.helpers import validate_config, ToggleDevice, Device
from homeassistant.const import (
EVENT_PLATFORM_DISCOVERED, CONF_ACCESS_TOKEN,
STATE_OPEN, STATE_CLOSED,
ATTR_SERVICE, ATTR_DISCOVERED, ATTR_FRIENDLY_NAME)
DOMAIN = "wink"
DEPENDENCIES = []
DISCOVER_LIGHTS = "wink.lights"
DISCOVER_SWITCHES = "wink.switches"
DISCOVER_SENSORS = "wink.sensors"
def setup(hass, config):
""" Sets up the Wink component. """
logger = logging.getLogger(__name__)
if not validate_config(config, {DOMAIN: [CONF_ACCESS_TOKEN]}, logger):
return False
pywink.set_bearer_token(config[DOMAIN][CONF_ACCESS_TOKEN])
# Load components for the devices in the Wink that we support
for component_name, func_exists, discovery_type in (
('light', pywink.get_bulbs, DISCOVER_LIGHTS),
('switch', pywink.get_switches, DISCOVER_SWITCHES),
('sensor', pywink.get_sensors, DISCOVER_SENSORS)):
if func_exists():
component = get_component(component_name)
# Ensure component is loaded
if component.DOMAIN not in hass.components:
bootstrap.setup_component(hass, component.DOMAIN, config)
# Fire discovery event
hass.bus.fire(EVENT_PLATFORM_DISCOVERED, {
ATTR_SERVICE: discovery_type,
ATTR_DISCOVERED: {}
})
return True
class WinkSensorDevice(Device):
""" represents a wink sensor within home assistant. """
def __init__(self, wink):
self.wink = wink
@property
def state(self):
""" Returns the state. """
return STATE_OPEN if self.is_open else STATE_CLOSED
@property
def unique_id(self):
""" Returns the id of this wink switch """
return "{}.{}".format(self.__class__, self.wink.deviceId())
@property
def name(self):
""" Returns the name of the sensor if any. """
return self.wink.name()
@property
def state_attributes(self):
""" Returns optional state attributes. """
return {
ATTR_FRIENDLY_NAME: self.wink.name()
}
def update(self):
""" Update state of the sensor. """
self.wink.updateState()
@property
def is_open(self):
""" True if door is open. """
return self.wink.state()
class WinkToggleDevice(ToggleDevice):
""" represents a WeMo switch within home assistant. """
def __init__(self, wink):
self.wink = wink
@property
def unique_id(self):
""" Returns the id of this WeMo switch """
return "{}.{}".format(self.__class__, self.wink.deviceId())
@property
def name(self):
""" Returns the name of the light if any. """
return self.wink.name()
@property
def is_on(self):
""" True if light is on. """
return self.wink.state()
@property
def state_attributes(self):
""" Returns optional state attributes. """
return {
ATTR_FRIENDLY_NAME: self.wink.name()
}
def turn_on(self, **kwargs):
""" Turns the switch on. """
self.wink.setState(True)
def turn_off(self):
""" Turns the switch off. """
self.wink.setState(False)
def update(self):
""" Update state of the light. """
self.wink.wait_till_desired_reached()

View File

@ -2,6 +2,9 @@
# Can be used to specify a catch all when registering state or event listeners.
MATCH_ALL = '*'
# If no name is specified
DEVICE_DEFAULT_NAME = "Unnamed Device"
# #### CONFIG ####
CONF_LATITUDE = "latitude"
CONF_LONGITUDE = "longitude"
@ -14,6 +17,8 @@ CONF_HOST = "host"
CONF_HOSTS = "hosts"
CONF_USERNAME = "username"
CONF_PASSWORD = "password"
CONF_API_KEY = "api_key"
CONF_ACCESS_TOKEN = "access_token"
# #### EVENTS ####
EVENT_HOMEASSISTANT_START = "homeassistant_start"
@ -22,12 +27,16 @@ EVENT_STATE_CHANGED = "state_changed"
EVENT_TIME_CHANGED = "time_changed"
EVENT_CALL_SERVICE = "call_service"
EVENT_SERVICE_EXECUTED = "service_executed"
EVENT_PLATFORM_DISCOVERED = "platform_discovered"
# #### STATES ####
STATE_ON = 'on'
STATE_OFF = 'off'
STATE_HOME = 'home'
STATE_NOT_HOME = 'not_home'
STATE_UNKNOWN = "unknown"
STATE_OPEN = 'open'
STATE_CLOSED = 'closed'
# #### STATE AND EVENT ATTRIBUTES ####
# Contains current time for a TIME_CHANGED event
@ -52,6 +61,16 @@ ATTR_ENTITY_PICTURE = "entity_picture"
# The unit of measurement if applicable
ATTR_UNIT_OF_MEASUREMENT = "unit_of_measurement"
# Temperature attribute
ATTR_TEMPERATURE = "temperature"
# #### MISC ####
TEMP_CELCIUS = "°C"
TEMP_FAHRENHEIT = "°F"
# Contains the information that is discovered
ATTR_DISCOVERED = "discovered"
# #### SERVICES ####
SERVICE_HOMEASSISTANT_STOP = "stop"

1
homeassistant/external/netdisco vendored Submodule

@ -0,0 +1 @@
Subproject commit 68877783cb989b874cbcaec5f388a8a4345891a6

1
homeassistant/external/noop vendored Submodule

@ -0,0 +1 @@
Subproject commit 45fae73c1f44342010fa07f3ed8909bf2819a508

@ -1 +1 @@
Subproject commit 991fbc9c42abe3e8bcce50fbf549d9fceefe1e29
Subproject commit e946ecf7926b9b2adaa1e3127a9738201a1b1fc7

@ -1 +1 @@
Subproject commit b86d410cd67ea1e3a60355aa23d17fe6761cb8c5
Subproject commit 7f6c383ded75f1273cbca28e858b8a8c96da66d4

408
homeassistant/external/wink/pywink.py vendored Normal file
View File

@ -0,0 +1,408 @@
__author__ = 'JOHNMCL'
import json
import time
import requests
baseUrl = "https://winkapi.quirky.com"
headers = {}
class wink_sensor_pod(object):
""" represents a wink.py sensor
json_obj holds the json stat at init (and if there is a refresh it's updated
it's the native format for this objects methods
and looks like so:
{
"data": {
"last_event": {
"brightness_occurred_at": None,
"loudness_occurred_at": None,
"vibration_occurred_at": None
},
"model_name": "Tripper",
"capabilities": {
"sensor_types": [
{
"field": "opened",
"type": "boolean"
},
{
"field": "battery",
"type": "percentage"
}
]
},
"manufacturer_device_model": "quirky_ge_tripper",
"location": "",
"radio_type": "zigbee",
"manufacturer_device_id": None,
"gang_id": None,
"sensor_pod_id": "37614",
"subscription": {
},
"units": {
},
"upc_id": "184",
"hidden_at": None,
"last_reading": {
"battery_voltage_threshold_2": 0,
"opened": False,
"battery_alarm_mask": 0,
"opened_updated_at": 1421697092.7347496,
"battery_voltage_min_threshold_updated_at": 1421697092.7347229,
"battery_voltage_min_threshold": 0,
"connection": None,
"battery_voltage": 25,
"battery_voltage_threshold_1": 25,
"connection_updated_at": None,
"battery_voltage_threshold_3": 0,
"battery_voltage_updated_at": 1421697092.7347066,
"battery_voltage_threshold_1_updated_at": 1421697092.7347302,
"battery_voltage_threshold_3_updated_at": 1421697092.7347434,
"battery_voltage_threshold_2_updated_at": 1421697092.7347374,
"battery": 1.0,
"battery_updated_at": 1421697092.7347553,
"battery_alarm_mask_updated_at": 1421697092.734716
},
"triggers": [
],
"name": "MasterBathroom",
"lat_lng": [
37.550773,
-122.279182
],
"uuid": "a2cb868a-dda3-4211-ab73-fc08087aeed7",
"locale": "en_us",
"device_manufacturer": "quirky_ge",
"created_at": 1421523277,
"local_id": "2",
"hub_id": "88264"
},
}
"""
def __init__(self, aJSonObj, objectprefix="sensor_pods"):
self.jsonState = aJSonObj
self.objectprefix = objectprefix
def __str__(self):
return "%s %s %s" % (self.name(), self.deviceId(), self.state())
def __repr__(self):
return "<Wink sensor %s %s %s>" % (self.name(), self.deviceId(), self.state())
@property
def _last_reading(self):
return self.jsonState.get('last_reading') or {}
def name(self):
return self.jsonState.get('name', "Unknown Name")
def state(self):
return self._last_reading.get('opened', False)
def deviceId(self):
return self.jsonState.get('sensor_pod_id', self.name())
def refresh_state_at_hub(self):
"""
Tell hub to query latest status from device and upload to Wink.
PS: Not sure if this even works..
"""
urlString = baseUrl + "/%s/%s/refresh" % (self.objectprefix, self.deviceId())
requests.get(urlString, headers=headers)
def updateState(self):
""" Update state with latest info from Wink API. """
urlString = baseUrl + "/%s/%s" % (self.objectprefix, self.deviceId())
arequest = requests.get(urlString, headers=headers)
self._updateStateFromResponse(arequest.json())
def _updateStateFromResponse(self, response_json):
"""
:param response_json: the json obj returned from query
:return:
"""
self.jsonState = response_json.get('data')
class wink_binary_switch(object):
""" represents a wink.py switch
json_obj holds the json stat at init (and if there is a refresh it's updated
it's the native format for this objects methods
and looks like so:
{
"data": {
"binary_switch_id": "4153",
"name": "Garage door indicator",
"locale": "en_us",
"units": {},
"created_at": 1411614982,
"hidden_at": null,
"capabilities": {},
"subscription": {},
"triggers": [],
"desired_state": {
"powered": false
},
"manufacturer_device_model": "leviton_dzs15",
"manufacturer_device_id": null,
"device_manufacturer": "leviton",
"model_name": "Switch",
"upc_id": "94",
"gang_id": null,
"hub_id": "11780",
"local_id": "9",
"radio_type": "zwave",
"last_reading": {
"powered": false,
"powered_updated_at": 1411614983.6153464,
"powering_mode": null,
"powering_mode_updated_at": null,
"consumption": null,
"consumption_updated_at": null,
"cost": null,
"cost_updated_at": null,
"budget_percentage": null,
"budget_percentage_updated_at": null,
"budget_velocity": null,
"budget_velocity_updated_at": null,
"summation_delivered": null,
"summation_delivered_updated_at": null,
"sum_delivered_multiplier": null,
"sum_delivered_multiplier_updated_at": null,
"sum_delivered_divisor": null,
"sum_delivered_divisor_updated_at": null,
"sum_delivered_formatting": null,
"sum_delivered_formatting_updated_at": null,
"sum_unit_of_measure": null,
"sum_unit_of_measure_updated_at": null,
"desired_powered": false,
"desired_powered_updated_at": 1417893563.7567682,
"desired_powering_mode": null,
"desired_powering_mode_updated_at": null
},
"current_budget": null,
"lat_lng": [
38.429996,
-122.653721
],
"location": "",
"order": 0
},
"errors": [],
"pagination": {}
}
"""
def __init__(self, aJSonObj, objectprefix="binary_switches"):
self.jsonState = aJSonObj
self.objectprefix = objectprefix
# Tuple (desired state, time)
self._last_call = (0, None)
def __str__(self):
return "%s %s %s" % (self.name(), self.deviceId(), self.state())
def __repr__(self):
return "<Wink switch %s %s %s>" % (self.name(), self.deviceId(), self.state())
@property
def _last_reading(self):
return self.jsonState.get('last_reading') or {}
def name(self):
return self.jsonState.get('name', "Unknown Name")
def state(self):
# Optimistic approach to setState:
# Within 15 seconds of a call to setState we assume it worked.
if self._recent_state_set():
return self._last_call[1]
return self._last_reading.get('powered', False)
def deviceId(self):
return self.jsonState.get('binary_switch_id', self.name())
def setState(self, state):
"""
:param state: a boolean of true (on) or false ('off')
:return: nothing
"""
urlString = baseUrl + "/%s/%s" % (self.objectprefix, self.deviceId())
values = {"desired_state": {"powered": state}}
arequest = requests.put(urlString, data=json.dumps(values), headers=headers)
self._updateStateFromResponse(arequest.json())
self._last_call = (time.time(), state)
def refresh_state_at_hub(self):
"""
Tell hub to query latest status from device and upload to Wink.
PS: Not sure if this even works..
"""
urlString = baseUrl + "/%s/%s/refresh" % (self.objectprefix, self.deviceId())
requests.get(urlString, headers=headers)
def updateState(self):
""" Update state with latest info from Wink API. """
urlString = baseUrl + "/%s/%s" % (self.objectprefix, self.deviceId())
arequest = requests.get(urlString, headers=headers)
self._updateStateFromResponse(arequest.json())
def wait_till_desired_reached(self):
""" Wait till desired state reached. Max 10s. """
if self._recent_state_set():
return
# self.refresh_state_at_hub()
tries = 1
while True:
self.updateState()
last_read = self._last_reading
if last_read.get('desired_powered') == last_read.get('powered') \
or tries == 5:
break
time.sleep(2)
tries += 1
self.updateState()
last_read = self._last_reading
def _updateStateFromResponse(self, response_json):
"""
:param response_json: the json obj returned from query
:return:
"""
self.jsonState = response_json.get('data')
def _recent_state_set(self):
return time.time() - self._last_call[0] < 15
class wink_bulb(wink_binary_switch):
""" represents a wink.py bulb
json_obj holds the json stat at init (and if there is a refresh it's updated
it's the native format for this objects methods
and looks like so:
"light_bulb_id": "33990",
"name": "downstaurs lamp",
"locale": "en_us",
"units":{},
"created_at": 1410925804,
"hidden_at": null,
"capabilities":{},
"subscription":{},
"triggers":[],
"desired_state":{"powered": true, "brightness": 1},
"manufacturer_device_model": "lutron_p_pkg1_w_wh_d",
"manufacturer_device_id": null,
"device_manufacturer": "lutron",
"model_name": "Caseta Wireless Dimmer & Pico",
"upc_id": "3",
"hub_id": "11780",
"local_id": "8",
"radio_type": "lutron",
"linked_service_id": null,
"last_reading":{
"brightness": 1,
"brightness_updated_at": 1417823487.490747,
"connection": true,
"connection_updated_at": 1417823487.4907365,
"powered": true,
"powered_updated_at": 1417823487.4907532,
"desired_powered": true,
"desired_powered_updated_at": 1417823485.054675,
"desired_brightness": 1,
"desired_brightness_updated_at": 1417409293.2591703
},
"lat_lng":[38.429962, -122.653715],
"location": "",
"order": 0
"""
jsonState = {}
def __init__(self, ajsonobj):
super().__init__(ajsonobj, "light_bulbs")
def deviceId(self):
return self.jsonState.get('light_bulb_id', self.name())
def brightness(self):
return self._last_reading.get('brightness')
def setState(self, state, brightness=None):
"""
:param state: a boolean of true (on) or false ('off')
:return: nothing
"""
urlString = baseUrl + "/light_bulbs/%s" % self.deviceId()
values = {
"desired_state": {
"powered": state
}
}
if brightness is not None:
values["desired_state"]["brightness"] = brightness
urlString = baseUrl + "/light_bulbs/%s" % self.deviceId()
arequest = requests.put(urlString, data=json.dumps(values), headers=headers)
self._updateStateFromResponse(arequest.json())
self._last_call = (time.time(), state)
def __repr__(self):
return "<Wink Bulb %s %s %s>" % (
self.name(), self.deviceId(), self.state())
def get_devices(filter, constructor):
arequestUrl = baseUrl + "/users/me/wink_devices"
j = requests.get(arequestUrl, headers=headers).json()
items = j.get('data')
devices = []
for item in items:
id = item.get(filter)
if (id is not None and item.get("hidden_at") is None):
devices.append(constructor(item))
return devices
def get_bulbs():
return get_devices('light_bulb_id', wink_bulb)
def get_switches():
return get_devices('binary_switch_id', wink_binary_switch)
def get_sensors():
return get_devices('sensor_pod_id', wink_sensor_pod)
def is_token_set():
""" Returns if an auth token has been set. """
return bool(headers)
def set_bearer_token(token):
global headers
headers = {
"Content-Type": "application/json",
"Authorization": "Bearer {}".format(token)
}
if __name__ == "__main__":
sw = get_bulbs()
lamp = sw[3]
lamp.setState(False)

View File

@ -7,7 +7,21 @@ from homeassistant import NoEntitySpecifiedError
from homeassistant.loader import get_component
from homeassistant.const import (
ATTR_ENTITY_ID, STATE_ON, STATE_OFF, CONF_PLATFORM, CONF_TYPE)
ATTR_ENTITY_ID, STATE_ON, STATE_OFF, CONF_PLATFORM, CONF_TYPE,
DEVICE_DEFAULT_NAME)
from homeassistant.util import ensure_unique_string, slugify
def generate_entity_id(entity_id_format, name, current_ids=None, hass=None):
""" Generate a unique entity ID based on given entity IDs or used ids. """
if current_ids is None:
if hass is None:
raise RuntimeError("Missing required parameter currentids or hass")
current_ids = hass.states.entity_ids()
return ensure_unique_string(
entity_id_format.format(slugify(name)), current_ids)
def extract_entity_ids(hass, service):
@ -112,7 +126,9 @@ def config_per_platform(config, domain, logger):
config_key = "{} {}".format(domain, found)
def platform_devices_from_config(config, domain, hass, logger):
def platform_devices_from_config(config, domain, hass,
entity_id_format, logger):
""" Parses the config for specified domain.
Loads different platforms and retrieve domains. """
devices = []
@ -143,33 +159,66 @@ def platform_devices_from_config(config, domain, hass, logger):
devices.extend(p_devices)
if len(devices) == 0:
logger.error("No devices found for %s", domain)
# Setup entity IDs for each device
device_dict = {}
return devices
no_name_count = 0
for device in devices:
# Get the name or set to default if none given
name = device.name or DEVICE_DEFAULT_NAME
if name == DEVICE_DEFAULT_NAME:
no_name_count += 1
name = "{} {}".format(domain, no_name_count)
entity_id = generate_entity_id(
entity_id_format, name, device_dict.keys())
device.entity_id = entity_id
device_dict[entity_id] = device
return device_dict
class ToggleDevice(object):
""" ABC for devices that can be turned on and off. """
class Device(object):
""" ABC for Home Assistant devices. """
# pylint: disable=no-self-use
entity_id = None
@property
def unique_id(self):
""" Returns a unique id. """
return "{}.{}".format(self.__class__, id(self))
@property
def name(self):
""" Returns the name of the device. """
return self.get_name()
@property
def state(self):
""" Returns the state of the device. """
return self.get_state()
@property
def state_attributes(self):
""" Returns the state attributes. """
return {}
# 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 device if any. """
return None
return DEVICE_DEFAULT_NAME
def turn_on(self, **kwargs):
""" Turn the device on. """
pass
def turn_off(self, **kwargs):
""" Turn the device off. """
pass
def is_on(self):
""" True if device is on. """
return False
def get_state(self):
""" Returns state of the device. """
return "Unknown"
def get_state_attributes(self):
""" Returns optional state attributes. """
@ -186,12 +235,37 @@ class ToggleDevice(object):
"""
if self.entity_id is None:
raise NoEntitySpecifiedError(
"No entity specified for device {}".format(self.get_name()))
"No entity specified for device {}".format(self.name))
if force_refresh:
self.update()
state = STATE_ON if self.is_on() else STATE_OFF
return hass.states.set(self.entity_id, self.state,
self.state_attributes)
return hass.states.set(self.entity_id, state,
self.get_state_attributes())
def __eq__(self, other):
return (isinstance(other, Device) and
other.unique_id == self.unique_id)
class ToggleDevice(Device):
""" ABC for devices that can be turned on and off. """
# pylint: disable=no-self-use
@property
def state(self):
""" Returns the state. """
return STATE_ON if self.is_on else STATE_OFF
@property
def is_on(self):
""" True if device is on. """
return False
def turn_on(self, **kwargs):
""" Turn the device on. """
pass
def turn_off(self, **kwargs):
""" Turn the device off. """
pass

View File

@ -112,17 +112,11 @@ class HomeAssistant(ha.HomeAssistant):
self.states = StateMachine(self.bus, self.remote_api)
def start(self):
# If there is no local API setup but we do want to connect with remote
# We create a random password and set up a local api
# Ensure a local API exists to connect with remote
if self.local_api is None:
import homeassistant.components.http as http
import random
# pylint: disable=too-many-format-args
random_password = '{:30}'.format(random.randrange(16**30))
http.setup(
self, {http.DOMAIN: {http.CONF_API_PASSWORD: random_password}})
http.setup(self)
ha.Timer(self)

View File

@ -12,6 +12,8 @@ from datetime import datetime, timedelta
import re
import enum
import socket
import random
import string
from functools import wraps
RE_SANITIZE_FILENAME = re.compile(r'(~|\.\.|/|\\)')
@ -134,16 +136,16 @@ def convert(value, to_type, default=None):
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, .. """
string = preferred_string
test_string = preferred_string
current_strings = list(current_strings)
tries = 1
while string in current_strings:
while test_string in current_strings:
tries += 1
string = "{}_{}".format(preferred_string, tries)
test_string = "{}_{}".format(preferred_string, tries)
return string
return test_string
# Taken from: http://stackoverflow.com/a/11735897
@ -163,6 +165,15 @@ def get_local_ip():
return socket.gethostbyname(socket.gethostname())
# Taken from http://stackoverflow.com/a/23728630
def get_random_string(length=10):
""" Returns a random string with letters and digits. """
generator = random.SystemRandom()
source_chars = string.ascii_letters + string.digits
return ''.join(generator.choice(source_chars) for _ in range(length))
class OrderedEnum(enum.Enum):
""" Taken from Python 3.4.0 docs. """
# pylint: disable=no-init, too-few-public-methods
@ -289,19 +300,24 @@ class Throttle(object):
def wrapper(*args, **kwargs):
"""
Wrapper that allows wrapped to be called only once per min_time.
If we cannot acquire the lock, it is running so return None.
"""
with lock:
last_call = wrapper.last_call
# Check if method is never called or no_throttle is given
force = last_call is None or kwargs.pop('no_throttle', False)
if lock.acquire(False):
try:
last_call = wrapper.last_call
if force or datetime.now() - last_call > self.min_time:
# Check if method is never called or no_throttle is given
force = not last_call or kwargs.pop('no_throttle', False)
result = method(*args, **kwargs)
wrapper.last_call = datetime.now()
return result
else:
return None
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()
wrapper.last_call = None

View File

@ -6,12 +6,16 @@ reports=no
# locally-disabled - it spams too much
# duplicate-code - unavoidable
# cyclic-import - doesn't test if both import on load
# file-ignored - we ignore a file to work around a pylint bug
# abstract-class-little-used - Prevents from setting right foundation
# abstract-class-not-used - is flaky, should not show up but does
# unused-argument - generic callbacks and setup methods create a lot of warnings
disable=
locally-disabled,
duplicate-code,
cyclic-import,
file-ignored
abstract-class-little-used,
abstract-class-not-used,
unused-argument
[EXCEPTIONS]
overgeneral-exceptions=Exception,HomeAssistantError

View File

@ -3,6 +3,9 @@ requests>=2.0
# optional, needed for specific components
# discovery
zeroconf>=0.16.0
# sun
pyephem>=3.7
@ -18,5 +21,11 @@ pyuserinput>=0.1.9
# switch.tellstick, tellstick_sensor
tellcore-py>=1.0.4
# namp_tracker plugin
# device_tracker.nmap
python-libnmap
# notify.pushbullet
pushbullet.py>=0.7.1
# thermostat.nest
python-nest>=2.1

View File

@ -1,5 +0,0 @@
#!/bin/bash
pylint homeassistant
flake8 homeassistant --exclude bower_components,external
python3 -m unittest discover ha_test

View File

@ -1,3 +1,8 @@
# If current pwd is scripts, go 1 up.
if [ ${PWD##*/} == "scripts" ]; then
cd ..
fi
# To build the frontend, you need node, bower and vulcanize
# npm install -g bower vulcanize
@ -19,4 +24,10 @@ mv polymer/bower_components/polymer/polymer.html.bak polymer/bower_components/po
# Generate the MD5 hash of the new frontend
cd ..
echo '""" DO NOT MODIFY. Auto-generated by build_frontend script """' > frontend.py
echo 'VERSION = "'`md5 -q www_static/frontend.html`'"' >> frontend.py
if [ $(command -v md5) ]; then
echo 'VERSION = "'`md5 -q www_static/frontend.html`'"' >> frontend.py
elif [ $(command -v md5sum) ]; then
echo 'VERSION = "'`md5sum www_static/frontend.html | cut -c-32`'"' >> frontend.py
else
echo 'Could not find a MD5 utility'
fi

7
scripts/check_style Executable file
View File

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

6
scripts/run_tests Executable file
View File

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

8
scripts/update Executable file
View File

@ -0,0 +1,8 @@
# If current pwd is scripts, go 1 up.
if [ ${PWD##*/} == "scripts" ]; then
cd ..
fi
git pull --recurse-submodules=yes
git submodule update --init --recursive
python3 -m pip install -r requirements.txt

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