mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 13:17:32 +00:00
Merge branch 'dev' into component-thermostat
* dev: Use tuples instead of lists internally Use properties instead of getters for Device class Upgrade pushbullet.py to 0.7.1 Prevent devices from being discovered twice Update netdisco to latest version Update netdisco to latest version Updated requirements.txt for the discovery component Automatic discovery and setting up of devices Ensure groups always have unique entity id Rename ha_test folder to tests Make group component more flexible Reorganized the main to be more modular Updated PyWemo to latest version Fix warnings from flake8 and pylint Check flags in ARP table for NUD_REACHABLE before assuming a device is online. Fixes #18. Pull in PyWemo bugfixes
This commit is contained in:
commit
cac1f56b2d
3
.gitmodules
vendored
3
.gitmodules
vendored
@ -4,3 +4,6 @@
|
||||
[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
|
||||
|
@ -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 -m unittest discover tests
|
||||
after_success:
|
||||
- coveralls
|
||||
|
@ -51,6 +51,7 @@ class HomeAssistant(object):
|
||||
self.bus = EventBus(pool)
|
||||
self.services = ServiceRegistry(self.bus, pool)
|
||||
self.states = StateMachine(self.bus)
|
||||
self.components = []
|
||||
|
||||
self.config_dir = os.path.join(os.getcwd(), 'config')
|
||||
|
||||
@ -222,7 +223,7 @@ def _process_match_param(parameter):
|
||||
elif isinstance(parameter, list):
|
||||
return parameter
|
||||
else:
|
||||
return [parameter]
|
||||
return (parameter,)
|
||||
|
||||
|
||||
def _matcher(subject, pattern):
|
||||
@ -588,7 +589,7 @@ 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]
|
||||
|
||||
|
@ -18,19 +18,8 @@ except ImportError:
|
||||
from homeassistant import bootstrap
|
||||
|
||||
|
||||
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 +33,14 @@ def main():
|
||||
if import_fail:
|
||||
print(("Install dependencies by running: "
|
||||
"pip3 install -r requirements.txt"))
|
||||
exit()
|
||||
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))
|
||||
@ -68,6 +60,27 @@ def main():
|
||||
'to write a default one to {}').format(config_path))
|
||||
sys.exit()
|
||||
|
||||
return config_path
|
||||
|
||||
|
||||
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_dependencies()
|
||||
|
||||
config_dir = os.path.join(os.getcwd(), args.config)
|
||||
|
||||
config_path = ensure_config_path(config_dir)
|
||||
|
||||
hass = bootstrap.from_config_file(config_path)
|
||||
hass.start()
|
||||
hass.block_till_stopped()
|
||||
|
@ -19,6 +19,33 @@ import homeassistant.loader as loader
|
||||
import homeassistant.components as core_components
|
||||
|
||||
|
||||
_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)
|
||||
|
||||
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
|
||||
def from_config_dict(config, hass=None):
|
||||
"""
|
||||
@ -29,8 +56,6 @@ def from_config_dict(config, hass=None):
|
||||
if hass is None:
|
||||
hass = homeassistant.HomeAssistant()
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
loader.prepare(hass)
|
||||
|
||||
# Make a copy because we are mutating it.
|
||||
@ -42,12 +67,12 @@ 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
|
||||
|
||||
@ -57,22 +82,11 @@ def from_config_dict(config, hass=None):
|
||||
add_worker = True
|
||||
|
||||
for domain in loader.load_order_components(components):
|
||||
component = loader.get_component(domain)
|
||||
if setup_component(hass, domain, config):
|
||||
add_worker = add_worker and domain != "group"
|
||||
|
||||
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)
|
||||
if add_worker:
|
||||
hass.pool.add_worker()
|
||||
|
||||
return hass
|
||||
|
||||
@ -112,7 +126,7 @@ def from_config_file(config_path, hass=None, enable_logging=True):
|
||||
logging.getLogger('').addHandler(err_handler)
|
||||
|
||||
else:
|
||||
logging.getLogger(__name__).error(
|
||||
_LOGGER.error(
|
||||
"Unable to setup error log %s (access denied)", err_log_path)
|
||||
|
||||
# Read config
|
||||
|
@ -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,24 @@ 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)
|
||||
# pylint: disable=unused-argument
|
||||
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. """
|
||||
@ -194,10 +214,11 @@ def setup(hass, config):
|
||||
|
||||
def update_chromecast_states(time): # pylint: disable=unused-argument
|
||||
""" 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. """
|
||||
|
@ -111,19 +111,16 @@ class DeviceTracker(object):
|
||||
""" Triggers update of the device states. """
|
||||
self.update_devices(now)
|
||||
|
||||
dev_group = group.Group(hass, GROUP_NAME_ALL_DEVICES)
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
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)
|
||||
|
||||
|
@ -101,7 +101,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
|
||||
|
||||
|
88
homeassistant/components/discovery.py
Normal file
88
homeassistant/components/discovery.py
Normal file
@ -0,0 +1,88 @@
|
||||
"""
|
||||
Starts a service to scan in intervals for new devices.
|
||||
|
||||
Will emit EVENT_SERVICE_DISCOVERED whenever a new service has been discovered.
|
||||
|
||||
Knows which components handle certain types, will make sure they are
|
||||
loaded before the EVENT_SERVICE_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, ATTR_SERVICE
|
||||
|
||||
DOMAIN = "discovery"
|
||||
DEPENDENCIES = []
|
||||
|
||||
EVENT_SERVICE_DISCOVERED = "service_discovered"
|
||||
|
||||
ATTR_DISCOVERED = "discovered"
|
||||
|
||||
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 not isinstance(service, str):
|
||||
service = (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_SERVICE_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:
|
||||
if bootstrap.setup_component(hass, component, config):
|
||||
hass.pool.add_worker()
|
||||
|
||||
hass.bus.fire(EVENT_SERVICE_DISCOVERED, {
|
||||
ATTR_SERVICE: service,
|
||||
ATTR_DISCOVERED: info
|
||||
})
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
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
|
@ -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,98 @@ 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()
|
||||
|
||||
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)
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def update_group_state(entity_id, old_state, new_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 +190,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)
|
||||
|
@ -178,8 +178,7 @@ def setup(hass, config):
|
||||
update_lights_state(None)
|
||||
|
||||
# Track all lights in a group
|
||||
group.setup_group(
|
||||
hass, GROUP_NAME_ALL_LIGHTS, lights.keys(), False)
|
||||
group.Group(hass, GROUP_NAME_ALL_LIGHTS, lights.keys(), False)
|
||||
|
||||
def handle_light_service(service):
|
||||
""" Hande a turn light on or off service call. """
|
||||
|
@ -77,9 +77,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 +145,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)
|
||||
|
@ -50,7 +50,7 @@ def setup(hass, config):
|
||||
notify_service = notify_implementation.get_service(hass, config)
|
||||
|
||||
if notify_service is None:
|
||||
_LOGGER.error("Failed to initialize notificatino service %s",
|
||||
_LOGGER.error("Failed to initialize notification service %s",
|
||||
platform)
|
||||
|
||||
return False
|
||||
|
@ -22,16 +22,22 @@ def get_service(hass, config):
|
||||
|
||||
try:
|
||||
# pylint: disable=unused-variable
|
||||
from pushbullet import PushBullet # noqa
|
||||
from pushbullet import PushBullet, InvalidKeyError # noqa
|
||||
|
||||
except ImportError:
|
||||
_LOGGER.exception(
|
||||
"Unable to import pushbullet. "
|
||||
"Did you maybe not install the 'pushbullet' package?")
|
||||
"Did you maybe not install the 'pushbullet.py' package?")
|
||||
|
||||
return None
|
||||
|
||||
return PushBulletNotificationService(config[DOMAIN][CONF_API_KEY])
|
||||
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
|
||||
|
@ -6,12 +6,13 @@ 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
|
||||
from homeassistant.components import group, discovery
|
||||
|
||||
DOMAIN = 'switch'
|
||||
DEPENDENCIES = []
|
||||
@ -27,6 +28,11 @@ ATTR_CURRENT_POWER_MWH = "current_power_mwh"
|
||||
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
|
||||
|
||||
# Maps discovered services to their platforms
|
||||
DISCOVERY = {
|
||||
discovery.services.BELKIN_WEMO: 'wemo'
|
||||
}
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@ -58,21 +64,41 @@ def setup(hass, config):
|
||||
switches = platform_devices_from_config(
|
||||
config, DOMAIN, hass, ENTITY_ID_FORMAT, logger)
|
||||
|
||||
if not switches:
|
||||
return False
|
||||
|
||||
# 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.values():
|
||||
switch.update_ha_state(hass)
|
||||
for switch in switches.values():
|
||||
switch.update_ha_state(hass)
|
||||
|
||||
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[service]))
|
||||
|
||||
switch = platform.device_discovered(hass, config, info)
|
||||
|
||||
if switch is not None and switch not in switches.values():
|
||||
switch.entity_id = util.ensure_unique_string(
|
||||
ENTITY_ID_FORMAT.format(util.slugify(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.services.BELKIN_WEMO, switch_discovered)
|
||||
|
||||
def handle_switch_service(service):
|
||||
""" Handles calls to the switch services. """
|
||||
target_switches = [switches[entity_id] for entity_id
|
||||
@ -90,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,
|
||||
switches.keys(), False)
|
||||
|
||||
# Update state every 30 seconds
|
||||
hass.track_time_change(update_states, second=[0, 30])
|
||||
|
||||
|
@ -36,10 +36,24 @@ 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
|
||||
|
||||
@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(
|
||||
self.last_sent_command_mask)
|
||||
|
||||
return last_command == tc_constants.TELLSTICK_TURNON
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def turn_on(self, **kwargs):
|
||||
""" Turns the switch on. """
|
||||
@ -49,14 +63,3 @@ class TellstickSwitch(ToggleDevice):
|
||||
def turn_off(self, **kwargs):
|
||||
""" Turns the switch off. """
|
||||
self.tellstick.turn_off()
|
||||
|
||||
def is_on(self):
|
||||
""" True if switch is on. """
|
||||
last_command = self.tellstick.last_sent_command(
|
||||
self.last_sent_command_mask)
|
||||
|
||||
return last_command == tc_constants.TELLSTICK_TURNON
|
||||
|
||||
def get_state_attributes(self):
|
||||
""" Returns optional state attributes. """
|
||||
return self.state_attr
|
||||
|
@ -11,16 +11,9 @@ from homeassistant.components.switch import (
|
||||
def get_devices(hass, config):
|
||||
""" Find and return WeMo switches. """
|
||||
|
||||
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
|
||||
except ImportError:
|
||||
logging.getLogger(__name__).exception((
|
||||
"Failed to import pywemo. "
|
||||
"Did you maybe not run `git submodule init` "
|
||||
"and `git submodule update`?"))
|
||||
pywemo, _ = get_pywemo()
|
||||
|
||||
if pywemo is None:
|
||||
return []
|
||||
|
||||
logging.getLogger(__name__).info("Scanning for WeMo devices")
|
||||
@ -31,28 +24,53 @@ def get_devices(hass, config):
|
||||
if isinstance(switch, pywemo.Switch)]
|
||||
|
||||
|
||||
def device_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 None if device is None else WemoSwitch(device)
|
||||
|
||||
|
||||
def get_pywemo():
|
||||
""" Tries to import PyWemo. """
|
||||
try:
|
||||
# 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 None, None
|
||||
|
||||
|
||||
class WemoSwitch(ToggleDevice):
|
||||
""" represents a WeMo switch within home assistant. """
|
||||
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
|
||||
@ -64,3 +82,16 @@ 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(True)
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
""" Turns the switch on. """
|
||||
self.wemo.on()
|
||||
|
||||
def turn_off(self):
|
||||
""" Turns the switch off. """
|
||||
self.wemo.off()
|
||||
|
@ -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"
|
||||
@ -29,6 +32,7 @@ STATE_ON = 'on'
|
||||
STATE_OFF = 'off'
|
||||
STATE_HOME = 'home'
|
||||
STATE_NOT_HOME = 'not_home'
|
||||
STATE_UNKNOWN = "unknown"
|
||||
|
||||
# #### STATE AND EVENT ATTRIBUTES ####
|
||||
# Contains current time for a TIME_CHANGED event
|
||||
|
1
homeassistant/external/netdisco
vendored
Submodule
1
homeassistant/external/netdisco
vendored
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit 27026d1f4a13afceb794a176f01cad9c1b37dc3b
|
2
homeassistant/external/pywemo
vendored
2
homeassistant/external/pywemo
vendored
@ -1 +1 @@
|
||||
Subproject commit 687fc4930967da6b2aa258a0e6bb0c4026a1907c
|
||||
Subproject commit 7f6c383ded75f1273cbca28e858b8a8c96da66d4
|
@ -7,7 +7,8 @@ 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
|
||||
|
||||
|
||||
@ -146,20 +147,17 @@ def platform_devices_from_config(config, domain, hass,
|
||||
|
||||
devices.extend(p_devices)
|
||||
|
||||
if len(devices) == 0:
|
||||
logger.error("No devices found for %s", domain)
|
||||
|
||||
# Setup entity IDs for each device
|
||||
no_name_count = 1
|
||||
|
||||
device_dict = {}
|
||||
|
||||
for device in devices:
|
||||
name = device.get_name()
|
||||
no_name_count = 0
|
||||
|
||||
if name is None:
|
||||
name = "{} #{}".format(domain, no_name_count)
|
||||
for device in devices:
|
||||
name = device.name
|
||||
|
||||
if name == DEVICE_DEFAULT_NAME:
|
||||
no_name_count += 1
|
||||
name = "{} #{}".format(domain, no_name_count)
|
||||
|
||||
entity_id = ensure_unique_string(
|
||||
entity_id_format.format(slugify(name)),
|
||||
@ -177,9 +175,34 @@ class Device(object):
|
||||
|
||||
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 get_state(self):
|
||||
""" Returns state of the device. """
|
||||
@ -200,22 +223,32 @@ class Device(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()
|
||||
|
||||
return hass.states.set(self.entity_id, self.get_state(),
|
||||
self.get_state_attributes())
|
||||
return hass.states.set(self.entity_id, self.state,
|
||||
self.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
|
||||
|
||||
def get_state(self):
|
||||
@property
|
||||
def state(self):
|
||||
""" Returns the state. """
|
||||
return STATE_ON if self.is_on() else STATE_OFF
|
||||
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. """
|
||||
@ -224,7 +257,3 @@ class ToggleDevice(Device):
|
||||
def turn_off(self, **kwargs):
|
||||
""" Turn the device off. """
|
||||
pass
|
||||
|
||||
def is_on(self):
|
||||
""" True if device is on. """
|
||||
return False
|
||||
|
@ -3,6 +3,9 @@ requests>=2.0
|
||||
|
||||
# optional, needed for specific components
|
||||
|
||||
# discovery
|
||||
zeroconf>=0.16.0
|
||||
|
||||
# sun
|
||||
pyephem>=3.7
|
||||
|
||||
@ -18,8 +21,8 @@ pyuserinput>=0.1.9
|
||||
# switch.tellstick, tellstick_sensor
|
||||
tellcore-py>=1.0.4
|
||||
|
||||
# namp_tracker plugin
|
||||
# device_tracker.nmap
|
||||
python-libnmap
|
||||
|
||||
# pushbullet
|
||||
pushbullet.py>=0.5.0
|
||||
# notify.pushbullet
|
||||
pushbullet.py>=0.7.1
|
||||
|
@ -2,4 +2,4 @@
|
||||
|
||||
pylint homeassistant
|
||||
flake8 homeassistant --exclude bower_components,external
|
||||
python3 -m unittest discover ha_test
|
||||
python3 -m unittest discover tests
|
||||
|
@ -7,7 +7,7 @@ Provides a mock switch platform.
|
||||
Call init before using it in your tests to ensure clean test data.
|
||||
"""
|
||||
from homeassistant.const import STATE_ON, STATE_OFF
|
||||
from ha_test.helpers import MockToggleDevice
|
||||
from tests.helpers import MockToggleDevice
|
||||
|
||||
|
||||
DEVICES = []
|
@ -7,7 +7,7 @@ Provides a mock switch platform.
|
||||
Call init before using it in your tests to ensure clean test data.
|
||||
"""
|
||||
from homeassistant.const import STATE_ON, STATE_OFF
|
||||
from ha_test.helpers import MockToggleDevice
|
||||
from tests.helpers import MockToggleDevice
|
||||
|
||||
|
||||
DEVICES = []
|
@ -1,5 +1,5 @@
|
||||
"""
|
||||
ha_test.helper
|
||||
tests.helper
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
Helper method for writing tests.
|
||||
@ -8,7 +8,7 @@ import os
|
||||
|
||||
import homeassistant as ha
|
||||
from homeassistant.helpers import ToggleDevice
|
||||
from homeassistant.const import STATE_ON, STATE_OFF
|
||||
from homeassistant.const import STATE_ON, STATE_OFF, DEVICE_DEFAULT_NAME
|
||||
|
||||
|
||||
def get_test_home_assistant():
|
||||
@ -45,29 +45,37 @@ class MockModule(object):
|
||||
class MockToggleDevice(ToggleDevice):
|
||||
""" Provides a mock toggle device. """
|
||||
def __init__(self, name, state):
|
||||
self.name = name
|
||||
self.state = state
|
||||
self._name = name or DEVICE_DEFAULT_NAME
|
||||
self._state = state
|
||||
self.calls = []
|
||||
|
||||
def get_name(self):
|
||||
@property
|
||||
def name(self):
|
||||
""" Returns the name of the device if any. """
|
||||
self.calls.append(('get_name', {}))
|
||||
return self.name
|
||||
self.calls.append(('name', {}))
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
""" Returns the name of the device if any. """
|
||||
self.calls.append(('state', {}))
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
""" True if device is on. """
|
||||
self.calls.append(('is_on', {}))
|
||||
return self._state == STATE_ON
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
""" Turn the device on. """
|
||||
self.calls.append(('turn_on', kwargs))
|
||||
self.state = STATE_ON
|
||||
self._state = STATE_ON
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
""" Turn the device off. """
|
||||
self.calls.append(('turn_off', kwargs))
|
||||
self.state = STATE_OFF
|
||||
|
||||
def is_on(self):
|
||||
""" True if device is on. """
|
||||
self.calls.append(('is_on', {}))
|
||||
return self.state == STATE_ON
|
||||
self._state = STATE_OFF
|
||||
|
||||
def last_call(self, method=None):
|
||||
if method is None:
|
@ -1,5 +1,5 @@
|
||||
"""
|
||||
ha_test.test_component_chromecast
|
||||
tests.test_component_chromecast
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Tests Chromecast component.
|
||||
@ -79,12 +79,3 @@ class TestChromecast(unittest.TestCase):
|
||||
self.assertEqual(service_name, call.service)
|
||||
self.assertEqual(self.test_entity,
|
||||
call.data.get(ATTR_ENTITY_ID))
|
||||
|
||||
def test_setup(self):
|
||||
"""
|
||||
Test Chromecast setup.
|
||||
We do not have access to a Chromecast while testing so test errors.
|
||||
In an ideal world we would create a mock pychromecast API..
|
||||
"""
|
||||
self.assertFalse(chromecast.setup(
|
||||
self.hass, {chromecast.DOMAIN: {CONF_HOSTS: '127.0.0.1'}}))
|
@ -1,5 +1,5 @@
|
||||
"""
|
||||
ha_test.test_component_core
|
||||
tests.test_component_core
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Tests core compoments.
|
@ -1,5 +1,5 @@
|
||||
"""
|
||||
ha_test.test_component_demo
|
||||
tests.test_component_demo
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Tests demo component.
|
@ -1,5 +1,5 @@
|
||||
"""
|
||||
ha_test.test_component_group
|
||||
tests.test_component_group
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Tests the group compoments.
|
||||
@ -75,7 +75,7 @@ class TestComponentsDeviceTracker(unittest.TestCase):
|
||||
device_tracker.DOMAIN: {CONF_PLATFORM: 'test'}
|
||||
}))
|
||||
|
||||
def test_device_tracker(self):
|
||||
def test_writing_known_devices_file(self):
|
||||
""" Test the device tracker class. """
|
||||
scanner = loader.get_component(
|
||||
'device_tracker.test').get_scanner(None, None)
|
||||
@ -117,7 +117,6 @@ class TestComponentsDeviceTracker(unittest.TestCase):
|
||||
dev3 = device_tracker.ENTITY_ID_FORMAT.format('DEV3')
|
||||
|
||||
now = datetime.now()
|
||||
nowNext = now + timedelta(seconds=ha.TIMER_INTERVAL)
|
||||
nowAlmostMinGone = (now + device_tracker.TIME_DEVICE_NOT_FOUND -
|
||||
timedelta(seconds=1))
|
||||
nowMinGone = nowAlmostMinGone + timedelta(seconds=2)
|
@ -1,5 +1,5 @@
|
||||
"""
|
||||
ha_test.test_component_group
|
||||
tests.test_component_group
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Tests the group compoments.
|
||||
@ -9,7 +9,7 @@ import unittest
|
||||
import logging
|
||||
|
||||
import homeassistant as ha
|
||||
from homeassistant.const import STATE_ON, STATE_OFF, STATE_HOME, STATE_NOT_HOME
|
||||
from homeassistant.const import STATE_ON, STATE_OFF, STATE_HOME, STATE_UNKNOWN
|
||||
import homeassistant.components.group as group
|
||||
|
||||
|
||||
@ -40,38 +40,41 @@ class TestComponentsGroup(unittest.TestCase):
|
||||
""" Stop down stuff we started. """
|
||||
self.hass.stop()
|
||||
|
||||
def test_setup_group(self):
|
||||
""" Test setup_group method. """
|
||||
# Try to setup a group with mixed groupable states
|
||||
def test_setup_group_with_mixed_groupable_states(self):
|
||||
""" Try to setup a group with mixed groupable states """
|
||||
self.hass.states.set('device_tracker.Paulus', STATE_HOME)
|
||||
self.assertTrue(group.setup_group(
|
||||
group.setup_group(
|
||||
self.hass, 'person_and_light',
|
||||
['light.Bowl', 'device_tracker.Paulus']))
|
||||
['light.Bowl', 'device_tracker.Paulus'])
|
||||
|
||||
self.assertEqual(
|
||||
STATE_ON,
|
||||
self.hass.states.get(
|
||||
group.ENTITY_ID_FORMAT.format('person_and_light')).state)
|
||||
|
||||
# Try to setup a group with a non existing state
|
||||
self.assertNotIn('non.existing', self.hass.states.entity_ids())
|
||||
self.assertTrue(group.setup_group(
|
||||
def test_setup_group_with_a_non_existing_state(self):
|
||||
""" Try to setup a group with a non existing state """
|
||||
grp = group.setup_group(
|
||||
self.hass, 'light_and_nothing',
|
||||
['light.Bowl', 'non.existing']))
|
||||
self.assertEqual(
|
||||
STATE_ON,
|
||||
self.hass.states.get(
|
||||
group.ENTITY_ID_FORMAT.format('light_and_nothing')).state)
|
||||
['light.Bowl', 'non.existing'])
|
||||
|
||||
# Try to setup a group with non groupable states
|
||||
self.assertEqual(STATE_ON, grp.state.state)
|
||||
|
||||
def test_setup_group_with_non_groupable_states(self):
|
||||
self.hass.states.set('cast.living_room', "Plex")
|
||||
self.hass.states.set('cast.bedroom', "Netflix")
|
||||
self.assertFalse(
|
||||
group.setup_group(
|
||||
self.hass, 'chromecasts',
|
||||
['cast.living_room', 'cast.bedroom']))
|
||||
|
||||
# Try to setup an empty group
|
||||
self.assertFalse(group.setup_group(self.hass, 'nothing', []))
|
||||
grp = group.setup_group(
|
||||
self.hass, 'chromecasts',
|
||||
['cast.living_room', 'cast.bedroom'])
|
||||
|
||||
self.assertEqual(STATE_UNKNOWN, grp.state.state)
|
||||
|
||||
def test_setup_empty_group(self):
|
||||
""" Try to setup an empty group. """
|
||||
grp = group.setup_group(self.hass, 'nothing', [])
|
||||
|
||||
self.assertEqual(STATE_UNKNOWN, grp.state.state)
|
||||
|
||||
def test_monitor_group(self):
|
||||
""" Test if the group keeps track of states. """
|
||||
@ -159,3 +162,10 @@ class TestComponentsGroup(unittest.TestCase):
|
||||
|
||||
self.assertEqual(STATE_ON, group_state.state)
|
||||
self.assertFalse(group_state.attributes[group.ATTR_AUTO])
|
||||
|
||||
def test_groups_get_unique_names(self):
|
||||
""" Two groups with same name should both have a unique entity id. """
|
||||
grp1 = group.Group(self.hass, 'Je suis Charlie')
|
||||
grp2 = group.Group(self.hass, 'Je suis Charlie')
|
||||
|
||||
self.assertNotEqual(grp1.entity_id, grp2.entity_id)
|
@ -1,5 +1,5 @@
|
||||
"""
|
||||
ha_test.test_component_http
|
||||
tests.test_component_http
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Tests Home Assistant HTTP component does what it should do.
|
@ -1,5 +1,5 @@
|
||||
"""
|
||||
ha_test.test_component_switch
|
||||
tests.test_component_switch
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Tests switch component.
|
@ -1,5 +1,5 @@
|
||||
"""
|
||||
ha_test.test_component_sun
|
||||
tests.test_component_sun
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Tests Sun component.
|
@ -1,5 +1,5 @@
|
||||
"""
|
||||
ha_test.test_component_switch
|
||||
tests.test_component_switch
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Tests switch component.
|
||||
@ -7,7 +7,6 @@ Tests switch component.
|
||||
# pylint: disable=too-many-public-methods,protected-access
|
||||
import unittest
|
||||
|
||||
import homeassistant as ha
|
||||
import homeassistant.loader as loader
|
||||
from homeassistant.const import STATE_ON, STATE_OFF, CONF_PLATFORM
|
||||
import homeassistant.components.switch as switch
|
||||
@ -82,29 +81,12 @@ class TestSwitch(unittest.TestCase):
|
||||
self.assertTrue(switch.is_on(self.hass, self.switch_2.entity_id))
|
||||
self.assertTrue(switch.is_on(self.hass, self.switch_3.entity_id))
|
||||
|
||||
def test_setup(self):
|
||||
# Bogus config
|
||||
self.assertFalse(switch.setup(self.hass, {}))
|
||||
|
||||
self.assertFalse(switch.setup(self.hass, {switch.DOMAIN: {}}))
|
||||
|
||||
# Test with non-existing component
|
||||
self.assertFalse(switch.setup(
|
||||
self.hass, {switch.DOMAIN: {CONF_PLATFORM: 'nonexisting'}}
|
||||
))
|
||||
|
||||
def test_setup_two_platforms(self):
|
||||
""" Test with bad config. """
|
||||
# Test if switch component returns 0 switches
|
||||
test_platform = loader.get_component('switch.test')
|
||||
test_platform.init(True)
|
||||
|
||||
self.assertEqual(
|
||||
[], test_platform.get_switches(None, None))
|
||||
|
||||
self.assertFalse(switch.setup(
|
||||
self.hass, {switch.DOMAIN: {CONF_PLATFORM: 'test'}}
|
||||
))
|
||||
|
||||
# Test if we can load 2 platforms
|
||||
loader.set_component('switch.test2', test_platform)
|
||||
test_platform.init(False)
|
||||
|
@ -1,5 +1,5 @@
|
||||
"""
|
||||
ha_test.test_core
|
||||
tests.test_core
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
Provides tests to verify that Home Assistant core works.
|
@ -1,5 +1,5 @@
|
||||
"""
|
||||
ha_test.test_helpers
|
||||
tests.test_helpers
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Tests component helpers.
|
@ -1,5 +1,5 @@
|
||||
"""
|
||||
ha_ha_test.test_loader
|
||||
ha_tests.test_loader
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Provides tests to verify that we can load components.
|
@ -1,5 +1,5 @@
|
||||
"""
|
||||
ha_test.remote
|
||||
tests.remote
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
Tests Home Assistant remote methods and classes.
|
@ -1,5 +1,5 @@
|
||||
"""
|
||||
ha_test.test_util
|
||||
tests.test_util
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
Tests Home Assistant util methods.
|
Loading…
x
Reference in New Issue
Block a user