diff options
Diffstat (limited to 'hostnix/elmo/home-assistant.nix')
| -rw-r--r-- | hostnix/elmo/home-assistant.nix | 473 |
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"''; + }; + }; +} |
