FruitieX / homectl-server

A simple home automation server written in Rust
MIT License
80 stars 10 forks source link
hacktoberfest

homectl

Discord: https://discord.gg/xP2s6EY8rd

🚧 WORK IN PROGRESS 🚧

Note: this project is still in quite early stages. Regardless, I've been using this as a daily driver for over a year now. This is also my first "real" Rust project, which brings with it the usual caveats. Luckily refactoring Rust code is a fairly pleasant experience.

If you're not ready to get your hands dirty with Rust code, I would suggest trying out other alternatives for now.

Quick start

You should now have a demo/dummy homectl environment running.

To control your home automation systems, you need to edit the generated configuration file in /Settings.toml. See below sections for configuration instructions and examples.

Description

This project aims to unify home automation (HA) systems from different brands, and does so by assuming complete control over the individual systems. It brings some features that I felt are missing from consumer HA systems, and also other similar solutions to homectl:

Setup

Environment variables (optional):

Database setup (optional)

Sample configs for supported integrations:

You can refer to the sample config for an example on how to put these concepts together.

MQTT

Sample configuration:

[integrations.example]
plugin = "mqtt"
host = "mqtt.example.org"
port = 1883

# Note the + sign, which acts as wildcard in MQTT topics.
# This will subscribe to both light and sensor updates.
topic = "home/+/example/{id}"
topic_set = "home/lights/example/{id}/set"

Example of a supported MQTT message using the default settings:

{
  "id": "fd2a1757-931b-4e44-b8f9-2fc8981626c1",
  "name": "Kitchen table",
  "power": true,
  "brightness": 0.252,
  "color": {
    "x": 0.5574,
    "y": 0.3919
  },
  "capabilities": {
    "xy": true,
    "ct": {
      "start": 2000,
      "end": 6535
    }
  }
}

Optionally, you can change the shape of read/written MQTT messages by setting the following fields to valid JSON pointers:

[integrations.example]

...

id_field = "/id"
name_field = "/name"
color_field = "/color"
power_field = "/power"
brightness_field = "/brightness"
sensor_value_field = "/sensor_value"
transition_ms_field = "/transition_ms"
capabilities_field = "/capabilities"

Neato

[integrations.neato]
plugin = "neato"
email = "example@gmail.com"
password = "your-password-here"
cleaning_days = ["Mon", "Wed", "Fri"]
cleaning_time_start = "08:00"
cleaning_time_end = "19:00"

# Set to true for debugging
dummy = false

# Example routine that starts your Neato robot(s)
[routines.leave_home]
name = "Leave home"
rules = [
  { integration_id = "hue1", name = "Entryway switch button 4", state = { value = true } }
]
actions = [
  { action = "IntegrationAction", integration_id = "neato", payload = "clean_house" },
]

Wake on LAN

# Wake up your PC:s by MAC address, and optionally put them to sleep!
# Make sure you've set up Wake on LAN on your PC.
# (optional) Set up sleep on lan: https://github.com/SR-G/sleep-on-lan
[integrations.wol]
plugin = "wake_on_lan"
machines = [
  { id = "office_pc", mac = "DE:AD:BE:EF:12:34", sleep_on_lan = "http://192.168.1.123:8009/sleep" }
]

# Example of a scene that turns on PC via WOL
[scenes.office]
name = "Office devices"

  [scenes.office.devices.wol]
  office_pc = { power = true }

Configuration tips / "recipes"

Group lights to control multiple lights at once:

[groups.entryway]
name = "Entryway"
devices = [
  { integration_id = "hue", name = "Entryway spot 1" },
  { integration_id = "hue", name = "Entryway spot 2" },
  { integration_id = "lifx", name = "Entryway table lamp" },
]

Combine groups into larger groups:

[groups.downstairs]
name = "Downstairs"
groups = [
  { group_id = "entryway" },
  { group_id = "kitchen" },
  { group_id = "office" },
]

I would suggest creating at least an "All" group containing all your devices.

Create scenes for setting lights to preset states:

[scenes.normal_downstairs]
name = "Normal (downstairs only)"

  # Add as many groups as you want
  [scenes.normal_downstairs.groups]
  downstairs = { power = true, color = { h = 42, s = 1.0 }}
  outdoor = { power = true, color = { r = 255, g = 255, b = 255 }, brightness = 0.75 }

  # You can add devices directly per integration and device name, too
  [scenes.normal_downstairs.devices.hue]
  "Hue lightstrip" = { power = true, color = { h = 263, s = 1.0 } }

Combine scenes into larger scenes:

[scenes.normal]
name = "Normal"

  # You can make a scene group/device use the state that it would have in
  # another scene
  [scenes.normal.groups]
  downstairs = { scene_id = "normal_downstairs" }
  upstairs = { scene_id = "normal_upstairs" }
  outdoor_spots = { scene_id = "outdoor" }

This is a bit of a niche feature, but I use it to create a scene for the entire house without needing to duplicate the config of contained scenes.

Make lights follow a fake circadian rhythm:

[integrations.circadian]
plugin = "circadian"
device_name = "Circadian rhythm"
day_color = { h = 25, s = 0.35 }
day_fade_start = "04:00"
day_fade_duration_hours = 4
night_color = { h = 20, s = 0.95 }
night_fade_start = "18:00"
night_fade_duration_hours = 3

[scenes.normal]
name = "Normal"

  # You can make a scene group/device use the state of another device
  [scenes.normal.groups]
  all = { integration_id = "circadian", device_id = "color" }

Make a light switch activate a scene:

[routines.arrive_home]
name = "Arrive home"
rules = [
  { integration_id = "hue", name = "Entryway switch button 1", state = { value = true } }
]
actions = [
  { action = "ActivateScene", scene_id = "normal_downstairs" },
]

Make a light switch cycle through a list of scenes:

[routines.favorite_scenes]
name = "Cycle through favorite scenes"
rules = [
  { integration_id = "hue1", name = "Living room switch button 1", state = { value = true } }
]
actions = [
  { action = "CycleScenes", scenes = [ { scene_id = "normal" }, { scene_id = "bright" } ] },
]

Make a light switch dim/brighten lights:

# Brighten
[routines.brighten]
name = "Brighten"
rules = [
  { integration_id = "hue1", name = "Living room switch button 2", state = { value = true } }
]
actions = [
  { action = "DimAction", step = -0.1 },
]

# Dim
[routines.dim]
name = "Dim"
rules = [
  { integration_id = "hue1", name = "Living room switch button 3", state = { value = true } }
]
actions = [
  { action = "DimAction", step = 0.1 },
]

Temporarily disable a motion detector when leaving the house:

[integrations.entryway_timer]
plugin = "timer"
device_name = "Entryway timer"

# This routine is triggered and turns on the lights when a motion sensor detects
# movement, and `entryway_timer` is not running.
[routines.entryway]
name = "Entryway"
rules = [
  { integration_id = "hue1", name = "Entryway motion sensor", state = { value = true } },
  { integration_id = "entryway_timer", name = "Entryway timer", state = { value = false } },
]
actions = [
  { action = "ActivateScene", scene_id = "normal_downstairs" },
  { action = "ActivateScene", scene_id = "outdoor" }
]

# This routine is triggered when switching off all lights via a switch located
# in the entryway. It activates a scene that turns off all lights, and starts
# `entryway_timer` with a timeout of 300 seconds.
[routines.leave_home]
name = "Leave home"
rules = [
  { integration_id = "hue1", name = "Entryway switch button 4", state = { value = true } }
]
actions = [
  { action = "ActivateScene", scene_id = "leave" },
  { action = "IntegrationAction", integration_id = "entryway_timer", payload = "300000" }

  # Clean the house too!
  { action = "IntegrationAction", integration_id = "neato", payload = "clean_house" },
]

Enable a motion sensor only when lights controlled by it are in a certain state:

# Activates `normal_upstairs` scene when motion is detected in staircase only if
# all upstairs lights are off
[routines.staircase_upstairs]
name = "Staircase (upstairs)"
rules = [
  { integration_id = "hue1", name = "Staircase motion sensor", state = { value = true } },
  { group_id = "upstairs", power = false }
]
actions = [
  { action = "ActivateScene", scene_id = "normal_upstairs" },
]

# Activates `normal_downstairs` scene when motion is detected in staircase only
# if all downstairs lights are off
[routines.staircase_downstairs]
name = "Staircase (downstairs)"
rules = [
  { integration_id = "hue1", name = "Staircase motion sensor", state = { value = true } },
  { group_id = "downstairs", power = false }
]
actions = [
  { action = "ActivateScene", scene_id = "normal_downstairs" }
]

My motivation for this setup is that the only task my motion sensor should perform is turning on the lights if they were previously off.

For instance if I have manually enabled another scene, I don't want that scene overwritten every time someone uses the stairs. Or if I'm setting the colors of my lights through the homectl UI, I don't want the changes to be lost whenever I walk past a motion detector.

Development notes

You can test features without access to physical hardware with configs such as:

[integrations.mqtt]
plugin = "mqtt"
host = "localhost"
port = 1883
topic = "home/devices/adb/{id}"
topic_set = "home/devices/adb/{id}/set"

[integrations.dummy]
plugin = "dummy"

[integrations.dummy.devices.sensor]
name = "Test sensor"
init_state = { Sensor = { OnOffSensor = { value = false } } }

[routines.test]
name = "Test routine"
rules = [
  { integration_id = "dummy", name = "Test sensor", state = { value = true } }
]
actions = [{ action = "Custom", integration_id = "mqtt", payload = '{ "topic": "home/devices/adb/android-tv/set", "json": "{ \"power\": false }" }' }]

Now you can test the Test routine by toggling the dummy sensor on/off over HTTP:

xh PUT localhost:45289/api/v1/devices/sensor id=sensor name="Test sensor" integration_id=dummy state:='{ "Sensor": { "OnOffSensor": { "value": true }}}'
xh PUT localhost:45289/api/v1/devices/sensor id=sensor name="Test sensor" integration_id=dummy state:='{ "Sensor": { "OnOffSensor": { "value": false }}}'