Configuration goes now into a single directory
This commit is contained in:
parent
f24e9597fe
commit
d570aeef33
10 changed files with 175 additions and 41 deletions
10
.gitignore
vendored
10
.gitignore
vendored
|
@ -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
|
||||
|
|
48
README.md
48
README.md
|
@ -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
|
||||
------------
|
||||
|
|
25
config/custom_components/example.py
Normal file
25
config/custom_components/example.py
Normal 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
|
|
@ -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]
|
|
@ -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)
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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. "
|
||||
|
|
2
start.py
2
start.py
|
@ -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()
|
||||
|
|
Loading…
Add table
Reference in a new issue