{ 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; }; switch = [ { platform = "template"; switches = { midea_cool = { unique_id = "switch_midea_cool"; value_template = "{{ 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"; }; } ]; } # 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"''; }; }; }