summaryrefslogtreecommitdiff
path: root/hostnix/elmo/home-assistant.nix
diff options
context:
space:
mode:
Diffstat (limited to 'hostnix/elmo/home-assistant.nix')
-rw-r--r--hostnix/elmo/home-assistant.nix473
1 files changed, 473 insertions, 0 deletions
diff --git a/hostnix/elmo/home-assistant.nix b/hostnix/elmo/home-assistant.nix
new file mode 100644
index 0000000..5648a0f
--- /dev/null
+++ b/hostnix/elmo/home-assistant.nix
@@ -0,0 +1,473 @@
+{ lib, pkgs, ... }:
+
+let
+
+ ha = {
+ trigger = {
+ at = time: {
+ platform = "time";
+ at = time;
+ };
+ };
+
+ action = {
+ on = entity: {
+ service = "${lib.head (lib.strings.splitString "." entity)}.turn_on";
+ target.entity_id = entity;
+ };
+
+ off = entity: {
+ service = "${lib.head (lib.strings.splitString "." entity)}.turn_off";
+ target.entity_id = entity;
+ };
+
+ toggle = entity: {
+ service = "${lib.head (lib.strings.splitString "." entity)}.toggle";
+ target.entity_id = entity;
+ };
+ };
+
+ button = entity_id: action: {
+ inherit action;
+ id = "button_${entity_id}";
+ alias = "Button ${entity_id}";
+ trigger = {
+ inherit entity_id;
+ platform = "state";
+ to = "on";
+ };
+ };
+ };
+
+in
+{
+ services.postgresql = {
+ ensureDatabases = [ "hass" ];
+ ensureUsers = [
+ {
+ name = "hass";
+ ensureDBOwnership = true;
+ }
+ ];
+ };
+
+ services.home-assistant = {
+ enable = true;
+ extraPackages =
+ ps: with ps; [
+ aiohomekit
+ psycopg2
+ ];
+ extraComponents = [
+ "apple_tv"
+ "esphome"
+ "met"
+ "roku"
+ "spotify"
+ "vesync"
+ ];
+ customComponents = with pkgs.home-assistant-custom-components; [
+ adaptive_lighting
+ auth_oidc
+ tuya_local
+ ];
+
+ config = {
+ default_config = { };
+ http = {
+ server_host = "::1";
+ trusted_proxies = [ "::1" ];
+ use_x_forwarded_for = true;
+ };
+ recorder.db_url = "postgresql://@/hass";
+
+ auth_oidc = {
+ client_id = "9332ad56-1917-4f12-a0ef-f6ff69994cf4";
+ discovery_url = "https://pi.mou.fo/.well-known/openid-configuration";
+ };
+ # "Smart" configured to channel 25 (some overlap with Wi-Fi channel 11).
+ zha = { };
+
+ adaptive_lighting = rec {
+ lights = [
+ # Unfortunately the grow light cannot have its own schedule.
+ "light.grow_light"
+ "light.panel_light"
+ ];
+ # Parameters modeled at https://basnijholt.github.io/adaptive-lighting/
+ min_color_temp = 2700; # lower bound of panel light
+ max_color_temp = 4300;
+ sunrise_offset = 60 * 60;
+ brightness_mode = "linear";
+ brightness_mode_time_dark = sunrise_offset;
+ brightness_mode_time_light = 3 * 60 * 60;
+ };
+
+ input_boolean = {
+ rain_today = {
+ name = "Rain today";
+ icon = "mdi:weather-rainy";
+ };
+ };
+
+ template = [
+ {
+ switch = [
+ {
+ unique_id = "switch_midea_cool";
+ name = "midea_cool";
+ state = "{{ is_state('climate.air_conditioner_1', 'cool') and state_attr('climate.air_conditioner_1', 'temperature')|float < 72 }}";
+ turn_on = [
+ {
+ service = "climate.set_temperature";
+ target.entity_id = "climate.air_conditioner_1";
+ data = {
+ hvac_mode = "cool";
+ temperature = 70;
+ };
+ }
+ ];
+ turn_off = [ (ha.action.off "climate.air_conditioner_1") ];
+ }
+ ];
+ }
+ ];
+
+ climate = [
+ {
+ unique_id = "climate_bedroom_heat";
+ name = "Bedroom Heat";
+ platform = "generic_thermostat";
+ heater = "switch.thermostat_heat";
+ target_sensor = "sensor.bedroom_temperature";
+ min_cycle_duration.minutes = 2;
+ # Remember HVAC mode and periodically explicitly sync heater.
+ # initial_hvac_mode = "off"
+ keep_alive.minutes = 5;
+ # Note: winter schedule has been removed.
+ # (setTemperatureAt "06:00" 71)
+ # (setTemperatureAt "12:00" 70)
+ # (setTemperatureAt "19:00" 71)
+ # (setTemperatureAt "22:00" 69)
+ }
+ {
+ unique_id = "climate_bedroom_cool";
+ name = "Bedroom Cool";
+ platform = "generic_thermostat";
+ ac_mode = true;
+ heater = "switch.midea_cool";
+ target_sensor = "sensor.bedroom_temperature";
+ min_cycle_duration.minutes = 2;
+ }
+ ];
+
+ automation = [
+ {
+ id = "depart";
+ alias = "Depart";
+ trigger = {
+ platform = "zone";
+ entity_id = "person.joe";
+ zone = "zone.home";
+ event = "leave";
+ };
+ action = [
+ (ha.action.off "light.panel_light")
+ (ha.action.off "climate.bedroom_cool")
+ (ha.action.off "climate.air_conditioner_1")
+ ];
+ }
+
+ {
+ id = "arrive_sunrise";
+ alias = "Arrive/Sunrise";
+ trigger = [
+ {
+ platform = "sun";
+ event = "sunrise";
+ }
+ {
+ platform = "zone";
+ entity_id = "person.joe";
+ zone = "zone.home";
+ event = "enter";
+ }
+ ];
+ condition = [
+ {
+ condition = "zone";
+ entity_id = "person.joe";
+ zone = "zone.home";
+ }
+ ];
+ action = [
+ (ha.action.on "light.panel_light")
+ ];
+ }
+
+ {
+ id = "summer_thermostat";
+ alias = "Summer thermostat";
+ trigger = [
+ (ha.trigger.at "08:00")
+ (ha.trigger.at "22:00")
+ {
+ platform = "zone";
+ entity_id = "person.joe";
+ zone = "zone.home";
+ event = "enter";
+ }
+ # TODO toggle a helper
+ # {
+ # platform = "event";
+ # event_type = "ios.action_fired";
+ # event_data.actionName = "Homebound";
+ # }
+ # TODO maybe trigger if home and over 78?
+ ];
+ condition = [
+ {
+ condition = "template";
+ value_template = "is_state('person.joe', 'home') || trigger.platform == 'event'";
+ }
+ ];
+ action = [
+ # Turn off bedroom_cool if we're controlling air_conditioner_1.
+ {
+ "if" = [
+ {
+ condition = "time";
+ after = "08:00";
+ before = "22:00";
+ }
+ {
+ condition = "template";
+ value_template = "{{ not is_state('climate.bedroom_cool', 'off') }}";
+ }
+ ];
+ "then" = [
+ (ha.action.off "climate.bedroom_cool")
+ { delay = 5; } # allow effect on air_conditioner_1 to settle
+ ];
+ }
+
+ {
+ service = "climate.set_temperature";
+ target.entity_id = ''
+ {% if 8 < now().hour < 22 %}
+ climate.air_conditioner_1
+ {% else %}
+ climate.bedroom_cool
+ {% endif %}
+ '';
+ # TODO Can Homebound burst to 72 for an hour?
+ data_template = {
+ hvac_mode = "auto";
+ # hvac_mode = ''
+ # {% if 8 < now().hour < 22 %}
+ # auto
+ # {% else %}
+ # cool
+ # {% endif %}
+ # '';
+ temperature = ''
+ {% if 8 < now().hour < 22 %}
+ 75
+ {% else %}
+ 73
+ {% endif %}
+ '';
+ };
+ }
+ ];
+ }
+
+ # TODO remove?
+ # {
+ # alias = "Pre-wake";
+ # trigger = [ (ha.trigger.at "07:00") ];
+ # action = [
+ # (ha.action.off "climate.bedroom_cool")
+ # ];
+ # }
+ {
+ id = "grow_light_morning";
+ alias = "Grow light morning";
+ # TODO make configurable (snooze)
+ trigger = [ (ha.trigger.at "09:00") ];
+ condition = [
+ {
+ condition = "zone";
+ entity_id = "person.joe";
+ zone = "zone.home";
+ }
+ ];
+ action = [
+ (ha.action.on "light.grow_light")
+ (ha.action.on "switch.wakko")
+ # {
+ # service = "fan.set_percentage";
+ # target.entity_id = "fan.core_200s";
+ # data.percentage = 100;
+ # }
+ ];
+ }
+ {
+ id = "grow_light_night";
+ alias = "Grow light night";
+ trigger = [ (ha.trigger.at "20:00") ];
+ action = [ (ha.action.off "switch.wakko") ];
+ }
+
+ (ha.button "binary_sensor.bedroom_button_1" [
+ (ha.action.toggle "light.panel_light")
+ {
+ service = "fan.set_percentage";
+ target.entity_id = "fan.core_200s";
+ data.percentage = 66;
+ }
+ ])
+ (ha.button "binary_sensor.bedroom_button_2" (ha.action.toggle "switch.wakko"))
+
+ {
+ id = "fridge_door_ajar";
+ alias = "Fridge door ajar";
+ triggers = [
+ {
+ trigger = "state";
+ entity_id = [ "binary_sensor.fridge_door_sensor" ];
+ to = [ "on" ];
+ for.minutes = 2;
+ }
+ ];
+ actions = [
+ {
+ action = "notify.notify";
+ data = {
+ message = "Check fridge door";
+ data = {
+ tag = "fridge-door";
+ push.interruption_level = "critical";
+ };
+ };
+ }
+ (ha.action.toggle "light.panel_light")
+ { delay.milliseconds = 500; }
+ (ha.action.toggle "light.panel_light")
+ ];
+ }
+ {
+ id = "fridge_door_closed";
+ alias = "Fridge door closed";
+ triggers = [
+ {
+ trigger = "state";
+ entity_id = [ "binary_sensor.fridge_door_sensor" ];
+ to = [ "off" ];
+ }
+ ];
+ actions = [
+ {
+ action = "notify.notify";
+ data = {
+ message = "clear_notification";
+ data.tag = "fridge-door";
+ };
+ }
+ ];
+ }
+
+ {
+ id = "check_rain_forecast";
+ alias = "Check rain forecast";
+ trigger = [ (ha.trigger.at "05:00") ];
+ action = [
+ {
+ action = "weather.get_forecasts";
+ target.entity_id = "weather.forecast_home";
+ data.type = "daily";
+ response_variable = "forecast";
+ }
+ {
+ "if" = [
+ {
+ condition = "template";
+ value_template = "{{ forecast['weather.forecast_home'].forecast[0].precipitation > 0 }}";
+ }
+ ];
+ "then" = [
+ {
+ action = "input_boolean.turn_on";
+ target.entity_id = "input_boolean.rain_today";
+ }
+ {
+ action = "notify.notify";
+ data.message = "Rain expected today ({{ forecast['weather.forecast_home'].forecast[0].precipitation }} inches)";
+ }
+ ];
+ "else" = [
+ {
+ action = "input_boolean.turn_off";
+ target.entity_id = "input_boolean.rain_today";
+ }
+ ];
+ }
+ ];
+ }
+
+ # The panel light can become unresponsive and need to be reboot.
+ {
+ id = "panel_light_reinitialize";
+ alias = "Panel light reinitialize";
+ trigger = [
+ {
+ platform = "state";
+ entity_id = [ "light.panel_light" ];
+ to = "unavailable";
+ for = "00:05:00";
+ }
+ ];
+ action = [
+ {
+ repeat = {
+ while = [
+ {
+ condition = "state";
+ entity_id = "light.panel_light";
+ state = "unavailable";
+ }
+ ];
+ sequence = [
+ (ha.action.off "switch.pinky")
+ { delay = 10; }
+ (ha.action.on "switch.pinky")
+ { delay = 30; }
+ ];
+ };
+ }
+ ];
+ }
+ ];
+ };
+ };
+
+ services.nginx.virtualHosts."ha.mou.fo" = {
+ enableACME = true;
+ forceSSL = true;
+ locations."/" = {
+ proxyPass = "http://[::1]:8123";
+ proxyWebsockets = true;
+ extraConfig = ''
+ # This is frequently used in examples but without clear explanation. It
+ # might help with WebSockets.
+ proxy_buffering off;
+ '';
+ };
+ # Disable service worker caching that works improperly with reverse proxy.
+ # https://github.com/home-assistant/frontend/issues/14836
+ # https://community.home-assistant.io/t/disabling-service-worker-reverse-proxy-auth-causes-issues/167082
+ locations."/service_worker.js" = {
+ return = ''410 "Service worker disabled: https://github.com/home-assistant/frontend/issues/14836"'';
+ };
+ };
+}