Configuration goes now into a single directory

This commit is contained in:
Paulus Schoutsen 2014-09-20 21:19:39 -05:00
parent f24e9597fe
commit d570aeef33
10 changed files with 175 additions and 41 deletions

10
.gitignore vendored
View File

@ -1,6 +1,10 @@
home-assistant.log config/*
home-assistant.conf !config/home-assistant.conf.default
known_devices.csv
# There is not a better solution afaik..
!config/custom_components
config/custom_components/*
!config/custom_components/example.py
# Hide sublime text stuff # Hide sublime text stuff
*.sublime-project *.sublime-project

View File

@ -30,16 +30,50 @@ Current compatible devices:
The system is built modular so support for other devices or actions can be implemented easily. The system is built modular so support for other devices or actions can be implemented easily.
Installation instructions Installation instructions / Quick-start guide
------------------------- ---------------------------------------------
* The core depends on [PyEphem](http://rhodesmill.org/pyephem/) and [Requests](http://python-requests.org). Depending on the components you would like to use you will need [PHue](https://github.com/studioimaginaire/phue) for Philips Hue support and [PyChromecast](https://github.com/balloob/pychromecast) for Chromecast support. Install these using `pip install pyephem requests phue pychromecast`. * The core depends on [PyEphem](http://rhodesmill.org/pyephem/) and [Requests](http://python-requests.org). Depending on the built-in components you would like to use you will need [PHue](https://github.com/studioimaginaire/phue) for Philips Hue support and [PyChromecast](https://github.com/balloob/pychromecast) for Chromecast support. Install these using `pip3 install pyephem requests phue pychromecast`.
* Clone the repository and pull in the submodules `git clone --recursive https://github.com/balloob/home-assistant.git` * Clone the repository and pull in the submodules `git clone --recursive https://github.com/balloob/home-assistant.git`
* Copy home-assistant.conf.default to home-assistant.conf and adjust the config values to match your setup. * In the config directory, copy home-assistant.conf.default to home-assistant.conf and adjust the config values to match your setup.
* For Tomato you will have to not only setup your host, username and password but also a http_id. The http_id can be retrieved by going to the admin console of your router, view the source of any of the pages and search for `http_id`. * For routers running Tomato you will have to not only setup your host, username and password but also a http_id. The http_id can be retrieved by going to the admin console of your router, view the source of any of the pages and search for `http_id`.
* If you want to use Hue, setup PHue by running `python -m phue --host HUE_BRIDGE_IP_ADDRESS` from the commandline and follow the instructions. * If you want to use Hue, setup PHue by running `python -m phue --host HUE_BRIDGE_IP_ADDRESS --config-file-path phue.conf` from the commandline inside your config directory and follow the instructions.
* While running the script it will create and maintain a file called `known_devices.csv` which will contain the detected devices. Adjust the track variable for the devices you want the script to act on and restart the script or call the service `device_tracker/reload_devices_csv`. * While running the script it will create and maintain a file called `known_devices.csv` which will contain the detected devices. Adjust the track variable for the devices you want the script to act on and restart the script or call the service `device_tracker/reload_devices_csv`.
Done. Start it now by running `python start.py` Done. Start it now by running `python3 start.py`
Customizing Home Assistant
----------------------------
Home Assistant can be extended by components. Components can listen or trigger events and offer services. Components are written in Python and can do all the goodness that Python has to offer.
By default Home Assistant offers a bunch of built-in components but it is easy to built your own. An example component can be found in [`/config/custom_components/example.py`](https://github.com/balloob/home-assistant/blob/master/config/custom_components/example.py)
*Note:* Home Assistant will use the directory that contains your config file as the directory that holds your customizations. The included file `start.py` points this at the `/config` folder but this can be anywhere on the filesystem.
A component can be loaded by referring to its name inside the config file. When loading a component Home Assistant will check the following paths:
* <config file directory>/custom_components/<component name>
* homeassistant/components/<component name> (built-in components)
Upon loading of a component a quick validation check will be done and only valid components will be loaded. Once loaded, a component will only be setup if all dependencies can be loaded and are able to setup.
*Warning:* You can override a built-in component by offering a component with the same name in your custom_components folder. This is not recommended and may lead to unexpected behavior!
A component is setup by passing in the Home Assistant object and a dict containing the configuration. The keys of the config-dict are components and the value is another dict with configuration attributes.
If your configuration file containes the following lines:
```
[example]
host=paulusschoutsen.nl
```
Then in the setup-method you will be able to refer to `config[example][host]` to get the value `paulusschoutsen.nl`.
Docker
------
A Docker image is available for Home Assistant. It will work with your current config directory and will always run the latest Home Assistant version. You can start it like this:
```
docker run -d --name="home-assistant" -v /path/to/homeassistant/config:/config -v /etc/localtime:/etc/localtime:ro -p 8123:8123 balloob/home-assistant
```
Architecture Architecture
------------ ------------

View File

@ -0,0 +1,25 @@
"""
custom_components.example
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Bare minimum what is needed for a component to be valid.
"""
DOMAIN = "example"
DEPENDENCIES = []
# pylint: disable=unused-argument
def setup(hass, config):
""" Register services or listen for events that your component needs. """
# Example of a service that prints the service call to the command-line.
hass.services.register(DOMAIN, "service_name", print)
# This prints a time change event to the command-line twice a minute.
hass.track_time_change(print, second=[0, 30])
# See also:
# hass.track_state_change
# hass.track_point_in_time
return True

View File

@ -40,3 +40,9 @@ download_dir=downloads
[process] [process]
# items are which processes to look for: <entity_id>=<search string within ps> # items are which processes to look for: <entity_id>=<search string within ps>
# xbmc=XBMC.App # xbmc=XBMC.App
[example]
[browser]
[keyboard]

View File

@ -6,6 +6,8 @@ Home Assistant is a Home Automation framework for observing the state
of entities and react to changes. of entities and react to changes.
""" """
import sys
import os
import time import time
import logging import logging
import threading import threading
@ -55,6 +57,25 @@ class HomeAssistant(object):
self.services = ServiceRegistry(self.bus, pool) self.services = ServiceRegistry(self.bus, pool)
self.states = StateMachine(self.bus) self.states = StateMachine(self.bus)
self._config_dir = os.getcwd()
@property
def config_dir(self):
""" Return value of config dir. """
return self._config_dir
@config_dir.setter
def config_dir(self, value):
""" Update value of config dir and ensures it's in Python path. """
self._config_dir = value
# Ensure we can load components from the config dir
sys.path.append(value)
def get_config_path(self, sub_path):
""" Returns path to the file within the config dir. """
return os.path.join(self._config_dir, sub_path)
def start(self): def start(self):
""" Start home assistant. """ """ Start home assistant. """
Timer(self) Timer(self)

View File

@ -7,6 +7,7 @@ After bootstrapping you can add your own components or
start by calling homeassistant.start_home_assistant(bus) start by calling homeassistant.start_home_assistant(bus)
""" """
import os
import configparser import configparser
import logging import logging
from collections import defaultdict from collections import defaultdict
@ -147,13 +148,20 @@ def from_config_file(config_path, hass=None, enable_logging=True):
functionality. Will add functionality to 'hass' parameter if given, functionality. Will add functionality to 'hass' parameter if given,
instantiates a new Home Assistant object if 'hass' is not given. instantiates a new Home Assistant object if 'hass' is not given.
""" """
if hass is None:
hass = homeassistant.HomeAssistant()
# Set config dir to directory holding config file
hass.config_dir = os.path.dirname(config_path)
if enable_logging: if enable_logging:
# Setup the logging for home assistant. # Setup the logging for home assistant.
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
# Log errors to a file # Log errors to a file
err_handler = logging.FileHandler("home-assistant.log", err_handler = logging.FileHandler(
mode='w', delay=True) hass.get_config_path("home-assistant.log"), mode='w', delay=True)
err_handler.setLevel(logging.ERROR) err_handler.setLevel(logging.ERROR)
err_handler.setFormatter( err_handler.setFormatter(
logging.Formatter('%(asctime)s %(name)s: %(message)s', logging.Formatter('%(asctime)s %(name)s: %(message)s',

View File

@ -44,20 +44,48 @@ SERVICE_MEDIA_PAUSE = "media_pause"
SERVICE_MEDIA_NEXT_TRACK = "media_next_track" SERVICE_MEDIA_NEXT_TRACK = "media_next_track"
SERVICE_MEDIA_PREV_TRACK = "media_prev_track" SERVICE_MEDIA_PREV_TRACK = "media_prev_track"
_COMPONENT_CACHE = {}
def get_component(component, logger=None):
def get_component(comp_name, logger=None):
""" Tries to load specified component.
Looks in config dir first, then built-in components.
Only returns it if also found to be valid. """
if comp_name in _COMPONENT_CACHE:
return _COMPONENT_CACHE[comp_name]
# First config dir, then built-in
potential_paths = ['custom_components.{}'.format(comp_name),
'homeassistant.components.{}'.format(comp_name)]
for path in potential_paths:
comp = _get_component(path, logger)
if comp is not None:
if logger is not None:
logger.info("Loaded component {} from {}".format(
comp_name, path))
_COMPONENT_CACHE[comp_name] = comp
return comp
# We did not find a component
if logger is not None:
logger.error(
"Failed to find component {}".format(comp_name))
return None
def _get_component(module, logger):
""" Tries to load specified component. """ Tries to load specified component.
Only returns it if also found to be valid.""" Only returns it if also found to be valid."""
try: try:
comp = importlib.import_module( comp = importlib.import_module(module)
'homeassistant.components.{}'.format(component))
except ImportError: except ImportError:
if logger:
logger.error(
"Failed to find component {}".format(component))
return None return None
# Validation if component has required methods and attributes # Validation if component has required methods and attributes
@ -75,7 +103,7 @@ def get_component(component, logger=None):
if errors: if errors:
if logger: if logger:
logger.error("Found invalid component {}: {}".format( logger.error("Found invalid component {}: {}".format(
component, ", ".join(errors))) module, ", ".join(errors)))
return None return None

View File

@ -119,6 +119,8 @@ class DeviceTracker(object):
self.lock = threading.Lock() self.lock = threading.Lock()
self.path_known_devices_file = hass.get_config_path(KNOWN_DEVICES_FILE)
# Dictionary to keep track of known devices and devices we track # Dictionary to keep track of known devices and devices we track
self.known_devices = {} self.known_devices = {}
@ -189,19 +191,21 @@ class DeviceTracker(object):
# known devices file # known devices file
if not self.invalid_known_devices_file: if not self.invalid_known_devices_file:
known_dev_path = self.path_known_devices_file
unknown_devices = [device for device in found_devices unknown_devices = [device for device in found_devices
if device not in known_dev] if device not in known_dev]
if unknown_devices: if unknown_devices:
try: try:
# If file does not exist we will write the header too # If file does not exist we will write the header too
is_new_file = not os.path.isfile(KNOWN_DEVICES_FILE) is_new_file = not os.path.isfile(known_dev_path)
with open(KNOWN_DEVICES_FILE, 'a') as outp: with open(known_dev_path, 'a') as outp:
self.logger.info(( self.logger.info((
"DeviceTracker:Found {} new devices," "DeviceTracker:Found {} new devices,"
" updating {}").format(len(unknown_devices), " updating {}").format(len(unknown_devices),
KNOWN_DEVICES_FILE)) known_dev_path))
writer = csv.writer(outp) writer = csv.writer(outp)
@ -221,7 +225,7 @@ class DeviceTracker(object):
except IOError: except IOError:
self.logger.exception(( self.logger.exception((
"DeviceTracker:Error updating {}" "DeviceTracker:Error updating {}"
"with {} new devices").format(KNOWN_DEVICES_FILE, "with {} new devices").format(known_dev_path,
len(unknown_devices))) len(unknown_devices)))
self.lock.release() self.lock.release()
@ -230,12 +234,12 @@ class DeviceTracker(object):
""" Parse and process the known devices file. """ """ Parse and process the known devices file. """
# Read known devices if file exists # Read known devices if file exists
if os.path.isfile(KNOWN_DEVICES_FILE): if os.path.isfile(self.path_known_devices_file):
self.lock.acquire() self.lock.acquire()
known_devices = {} known_devices = {}
with open(KNOWN_DEVICES_FILE) as inp: with open(self.path_known_devices_file) as inp:
default_last_seen = datetime(1990, 1, 1) default_last_seen = datetime(1990, 1, 1)
# Temp variable to keep track of which entity ids we use # Temp variable to keep track of which entity ids we use
@ -276,7 +280,7 @@ class DeviceTracker(object):
if not known_devices: if not known_devices:
self.logger.warning( self.logger.warning(
"No devices to track. Please update {}.".format( "No devices to track. Please update {}.".format(
KNOWN_DEVICES_FILE)) self.path_known_devices_file))
# Remove entities that are no longer maintained # Remove entities that are no longer maintained
new_entity_ids = set([known_devices[device]['entity_id'] new_entity_ids = set([known_devices[device]['entity_id']
@ -297,14 +301,14 @@ class DeviceTracker(object):
self.logger.info( self.logger.info(
"DeviceTracker:Loaded devices from {}".format( "DeviceTracker:Loaded devices from {}".format(
KNOWN_DEVICES_FILE)) self.path_known_devices_file))
except KeyError: except KeyError:
self.invalid_known_devices_file = True self.invalid_known_devices_file = True
self.logger.warning(( self.logger.warning((
"Invalid {} found. " "Invalid known devices file: {}. "
"We won't update it with new found devices." "We won't update it with new found devices."
).format(KNOWN_DEVICES_FILE)) ).format(self.path_known_devices_file))
finally: finally:
self.lock.release() self.lock.release()

View File

@ -87,6 +87,7 @@ ATTR_BRIGHTNESS = "brightness"
# String representing a profile (built-in ones or external defined) # String representing a profile (built-in ones or external defined)
ATTR_PROFILE = "profile" ATTR_PROFILE = "profile"
PHUE_CONFIG_FILE = "phue.conf"
LIGHT_PROFILES_FILE = "light_profiles.csv" LIGHT_PROFILES_FILE = "light_profiles.csv"
@ -156,7 +157,7 @@ def setup(hass, config):
return False return False
light_control = light_init(config[DOMAIN]) light_control = light_init(hass, config[DOMAIN])
ent_to_light = {} ent_to_light = {}
light_to_ent = {} light_to_ent = {}
@ -226,14 +227,15 @@ def setup(hass, config):
group.setup_group(hass, GROUP_NAME_ALL_LIGHTS, light_to_ent.values()) group.setup_group(hass, GROUP_NAME_ALL_LIGHTS, light_to_ent.values())
# Load built-in profiles and custom profiles # Load built-in profiles and custom profiles
profile_paths = [os.path.dirname(__file__), os.getcwd()] profile_paths = [os.path.join(os.path.dirname(__file__),
LIGHT_PROFILES_FILE),
hass.get_config_path(LIGHT_PROFILES_FILE)]
profiles = {} profiles = {}
for dir_path in profile_paths: for profile_path in profile_paths:
file_path = os.path.join(dir_path, LIGHT_PROFILES_FILE)
if os.path.isfile(file_path): if os.path.isfile(profile_path):
with open(file_path) as inp: with open(profile_path) as inp:
reader = csv.reader(inp) reader = csv.reader(inp)
# Skip the header # Skip the header
@ -249,7 +251,7 @@ def setup(hass, config):
# ValueError if convert to float/int failed # ValueError if convert to float/int failed
logger.error( logger.error(
"Error parsing light profiles from {}".format( "Error parsing light profiles from {}".format(
file_path)) profile_path))
return False return False
@ -353,7 +355,7 @@ def _hue_to_light_state(info):
class HueLightControl(object): class HueLightControl(object):
""" Class to interface with the Hue light system. """ """ Class to interface with the Hue light system. """
def __init__(self, config): def __init__(self, hass, config):
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
host = config.get(ha.CONF_HOST, None) host = config.get(ha.CONF_HOST, None)
@ -369,7 +371,9 @@ class HueLightControl(object):
return return
try: try:
self._bridge = phue.Bridge(host) self._bridge = phue.Bridge(host,
config_file_path=hass.get_config_path(
PHUE_CONFIG_FILE))
except socket.error: # Error connecting using Phue except socket.error: # Error connecting using Phue
logger.exception(( logger.exception((
"HueLightControl:Error while connecting to the bridge. " "HueLightControl:Error while connecting to the bridge. "

View File

@ -3,6 +3,6 @@
import homeassistant import homeassistant
import homeassistant.bootstrap import homeassistant.bootstrap
hass = homeassistant.bootstrap.from_config_file("home-assistant.conf") hass = homeassistant.bootstrap.from_config_file("config/home-assistant.conf")
hass.start() hass.start()
hass.block_till_stopped() hass.block_till_stopped()