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
home-assistant.conf
known_devices.csv
config/*
!config/home-assistant.conf.default
# There is not a better solution afaik..
!config/custom_components
config/custom_components/*
!config/custom_components/example.py
# Hide sublime text stuff
*.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.
Installation instructions
-------------------------
* 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`.
Installation instructions / Quick-start guide
---------------------------------------------
* 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`
* 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`.
* 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.
* In the config directory, copy home-assistant.conf.default to home-assistant.conf and adjust the config values to match your setup.
* 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 --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`.
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
------------

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]
# items are which processes to look for: <entity_id>=<search string within ps>
# 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.
"""
import sys
import os
import time
import logging
import threading
@ -55,6 +57,25 @@ class HomeAssistant(object):
self.services = ServiceRegistry(self.bus, pool)
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):
""" Start home assistant. """
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)
"""
import os
import configparser
import logging
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,
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:
# Setup the logging for home assistant.
logging.basicConfig(level=logging.INFO)
# Log errors to a file
err_handler = logging.FileHandler("home-assistant.log",
mode='w', delay=True)
err_handler = logging.FileHandler(
hass.get_config_path("home-assistant.log"), mode='w', delay=True)
err_handler.setLevel(logging.ERROR)
err_handler.setFormatter(
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_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.
Only returns it if also found to be valid."""
try:
comp = importlib.import_module(
'homeassistant.components.{}'.format(component))
comp = importlib.import_module(module)
except ImportError:
if logger:
logger.error(
"Failed to find component {}".format(component))
return None
# Validation if component has required methods and attributes
@ -75,7 +103,7 @@ def get_component(component, logger=None):
if errors:
if logger:
logger.error("Found invalid component {}: {}".format(
component, ", ".join(errors)))
module, ", ".join(errors)))
return None

View file

@ -119,6 +119,8 @@ class DeviceTracker(object):
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
self.known_devices = {}
@ -189,19 +191,21 @@ class DeviceTracker(object):
# 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
if device not in known_dev]
if unknown_devices:
try:
# 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((
"DeviceTracker:Found {} new devices,"
" updating {}").format(len(unknown_devices),
KNOWN_DEVICES_FILE))
known_dev_path))
writer = csv.writer(outp)
@ -221,7 +225,7 @@ class DeviceTracker(object):
except IOError:
self.logger.exception((
"DeviceTracker:Error updating {}"
"with {} new devices").format(KNOWN_DEVICES_FILE,
"with {} new devices").format(known_dev_path,
len(unknown_devices)))
self.lock.release()
@ -230,12 +234,12 @@ class DeviceTracker(object):
""" Parse and process the known devices file. """
# 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()
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)
# Temp variable to keep track of which entity ids we use
@ -276,7 +280,7 @@ class DeviceTracker(object):
if not known_devices:
self.logger.warning(
"No devices to track. Please update {}.".format(
KNOWN_DEVICES_FILE))
self.path_known_devices_file))
# Remove entities that are no longer maintained
new_entity_ids = set([known_devices[device]['entity_id']
@ -297,14 +301,14 @@ class DeviceTracker(object):
self.logger.info(
"DeviceTracker:Loaded devices from {}".format(
KNOWN_DEVICES_FILE))
self.path_known_devices_file))
except KeyError:
self.invalid_known_devices_file = True
self.logger.warning((
"Invalid {} found. "
"Invalid known devices file: {}. "
"We won't update it with new found devices."
).format(KNOWN_DEVICES_FILE))
).format(self.path_known_devices_file))
finally:
self.lock.release()

View file

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

View file

@ -3,6 +3,6 @@
import homeassistant
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.block_till_stopped()