{ lib, pkgs, ... }: { services.postgresql = { enable = true; ensureDatabases = [ "hass" ]; ensureUsers = [{ name = "hass"; ensureDBOwnership = true; }]; }; services.home-assistant = { enable = true; extraPackages = ps: with ps; [ psycopg2 ]; extraComponents = [ "apple_tv" "esphome" "met" "roku" "spotify" "vesync" ]; customComponents = with pkgs.home-assistant-custom-components; [ adaptive_lighting auth-header tuya_local ]; config = 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; }; }; }; in { default_config = { }; http = { server_host = "::1"; trusted_proxies = [ "::1" ]; use_x_forwarded_for = true; }; recorder.db_url = "postgresql://@/hass"; auth_header = { }; #binary_sensor: # - platform: template # sensors: # rain_today: # friendly_name: Rain today # value_template: "{{ state_attr('weather.knyc_daynight', 'forecast')[1]['precipitation_probability']|float > 0 or state_attr('weather.knyc_daynight', 'forecast')[2]['precipitation_probability']|float > 0 }}" # #value_template: "{{ states('sensor.dark_sky_precip_probability_0d')|float > 5 }}" 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 = 3500; 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 = { 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 = [ { platform = "generic_thermostat"; name = "Bedroom Heat"; 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) } { platform = "generic_thermostat"; name = "Bedroom Cool"; ac_mode = true; heater = "switch.midea_cool"; target_sensor = "sensor.bedroom_temperature"; min_cycle_duration.minutes = 2; } ]; # - alias: Sound machine switch # description: '' # trigger: # - device_id: 6abc7b035b2e42e6a620eb66ccc8de74 # domain: binary_sensor # entity_id: binary_sensor.button_2 # platform: device # type: turned_on # condition: [] # action: # - entity_id: switch.pinky # service: switch.toggle # id: f6405dfa252e4121b51ab015c7d5a136 # - alias: Light relay # description: '' # trigger: # - above: '.15' # entity_id: sensor.photoresistor # platform: numeric_state # condition: # - after: sunset # condition: sun # action: # - entity_id: switch.wakko # service: switch.turn_on # id: ce759a29adb74a6090dcb0d1c1ef704c # - alias: Bedroom thermostat off # description: '' # trigger: # - entity_id: climate.bedroom # platform: state # to: 'off' # condition: [] # action: # - entity_id: light.d5_led # service: light.turn_on # id: 3616bf440fe84c76bbfed07fa336272b # - alias: Bedroom thermostat on # description: '' # trigger: # - entity_id: climate.bedroom # from: 'off' # platform: state # condition: [] # action: # - entity_id: light.d5_led # service: light.turn_off # mode: single # id: 117ab3e24e624680aa265efa8b0986d2 # - alias: Rain light on # description: '' # trigger: # - entity_id: binary_sensor.rain_today # platform: state # to: 'on' # condition: [] # action: # - data: {} # entity_id: light.nodemcu_led # service: light.turn_on # id: 68401f3337c546e38521d77da2bd4040 # - alias: Rain light off # description: '' # trigger: # - entity_id: binary_sensor.rain_today # platform: state # to: 'off' # condition: [] # action: # - data: {} # entity_id: light.nodemcu_led # service: light.turn_off # id: ba3266d2ebfe4b4cabbc5ef36a63412d # - alias: Windows webhook # description: '' # trigger: # - platform: webhook # webhook_id: -5ykPDB93EXDiNKFAnaJ6_aZi # condition: [] # action: # - choose: # - conditions: # - condition: template # value_template: '{{ trigger.data.action == ''on'' }}' # sequence: # - repeat: # while: # - condition: template # value_template: '{{ state_attr(''media_player.55_tcl_roku_tv'', ''app_id'') # != ''tvinput.hdmi1'' }} # ' # - condition: template # value_template: '{{ repeat.index < 2 }}' # sequence: # - alias: Turn on TV to speed up switching source # service: media_player.turn_on # target: # entity_id: media_player.55_tcl_roku_tv # continue_on_error: true # - service: media_player.select_source # data: # source: tvinput.hdmi1 # target: # entity_id: media_player.55_tcl_roku_tv # continue_on_error: true # - wait_template: '{{ state_attr(''media_player.55_tcl_roku_tv'', ''app_id'') # == ''tvinput.hdmi1'' }}' # timeout: 10 # alias: Retry while (checks before) TV input not on Windows # - conditions: # - condition: template # value_template: '{{ trigger.data.action == ''off'' }}' # sequence: # - service: media_player.turn_off # data: {} # target: # entity_id: media_player.55_tcl_roku_tv # mode: restart # id: c759fbc37e4f46e0a834d95f40a78427 # - alias: Barrier webhook # description: '' # trigger: # - platform: webhook # webhook_id: barrier-webhook-GyQhMV4gPNhSfYbYFx4DNVUc # condition: [] # action: # - choose: # - conditions: # - condition: template # value_template: '{{ trigger.data.action == ''on'' }}' # sequence: # - repeat: # while: # - condition: template # value_template: '{{ state_attr(''media_player.55_tcl_roku_tv'', ''app_id'') # != ''tvinput.hdmi1'' }} # ' # - condition: template # value_template: '{{ repeat.index < 2 }}' # sequence: # - alias: Turn on TV to speed up switching source # service: media_player.turn_on # target: # entity_id: media_player.55_tcl_roku_tv # continue_on_error: true # - service: media_player.select_source # data: # source: tvinput.hdmi1 # target: # entity_id: media_player.55_tcl_roku_tv # continue_on_error: true # - wait_template: '{{ state_attr(''media_player.55_tcl_roku_tv'', ''app_id'') # == ''tvinput.hdmi1'' }}' # timeout: 10 # alias: Retry while (checks before) TV input not on Windows # - conditions: # - condition: template # value_template: '{{ trigger.data.action == ''hdmi2'' }}' # sequence: # - repeat: # while: # - condition: template # value_template: '{{ state_attr(''media_player.55_tcl_roku_tv'', ''app_id'') # != ''tvinput.hdmi2'' }} # ' # - condition: template # value_template: '{{ repeat.index < 2 }}' # sequence: # - alias: Turn on TV to speed up switching source # service: media_player.turn_on # target: # entity_id: media_player.55_tcl_roku_tv # continue_on_error: true # - service: media_player.select_source # data: # source: tvinput.hdmi2 # target: # entity_id: media_player.55_tcl_roku_tv # continue_on_error: true # - wait_template: '{{ state_attr(''media_player.55_tcl_roku_tv'', ''app_id'') # == ''tvinput.hdmi2'' }}' # timeout: 10 # alias: Retry while (checks before) TV input not on HDMI 2 # - conditions: # - condition: template # value_template: '{{ trigger.data.action == ''off'' }}' # sequence: # - service: media_player.turn_off # data: {} # target: # entity_id: media_player.55_tcl_roku_tv # mode: restart # id: a86bdf5640e34e5d9b4864f7b58d4bae # - id: '1678928586230' # alias: Spotify WIP # description: '' # trigger: # - platform: time # at: 08:55:00 # condition: # - condition: zone # entity_id: person.joe # zone: zone.home # action: # - repeat: # until: # - condition: or # conditions: # - condition: state # entity_id: media_player.55_tcl_roku_tv # attribute: source # state: Spotify Music # - condition: template # value_template: '{{ repeat.index >= 5 }}' # sequence: # - alias: Turn on TV to speed up switching source # service: media_player.turn_on # target: # entity_id: media_player.55_tcl_roku_tv # continue_on_error: true # - service: media_player.select_source # data: # source: Spotify Music # target: # entity_id: media_player.55_tcl_roku_tv # continue_on_error: true # - delay: # hours: 0 # minutes: 0 # seconds: 15 # milliseconds: 0 # - wait_template: '{{ state_attr(''media_player.spotify_yoyoyojomo'', ''source_list'') # | default([], true) | contains(''55" TCL Roku TV'') }}' # continue_on_timeout: true # timeout: '15:00' # - service: media_player.select_source # data: # source: 55" TCL Roku TV # target: # entity_id: media_player.spotify_yoyoyojomo # enabled: false # - service: media_player.play_media # data: # media_content_id: spotify:playlist:37i9dQZF1E355kum8XgZAW # media_content_type: playlist # target: # entity_id: media_player.spotify_yoyoyojomo # enabled: false # - stop: Power cycle TV for SPDIF glitch? # enabled: false # - repeat: # until: # - condition: or # conditions: # - condition: device # device_id: 41de89da6b761d29f8542ab039951a1b # domain: media_player # entity_id: media_player.spotify_yoyoyojomo # type: is_playing # - condition: template # value_template: '{{ repeat.index >= 5 }}' # sequence: # - service: media_player.select_source # data: # source: 55" TCL Roku TV # target: # entity_id: media_player.spotify_yoyoyojomo # - service: media_player.play_media # data: # media_content_id: spotify:playlist:37i9dQZEVXbgyUT3Ewvdog # media_content_type: playlist # target: # entity_id: media_player.spotify_yoyoyojomo # continue_on_error: true # - delay: # hours: 0 # minutes: 0 # seconds: 20 # milliseconds: 0 # - stop: 'Release Radar (updates Fridays): 37i9dQZEVXbgyUT3Ewvdog' # mode: single automation = let button = entity_id: action: { inherit action; alias = "Button ${entity_id}"; trigger = { inherit entity_id; platform = "state"; to = "on"; }; }; in [ { 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") ]; } { 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") ]; } # TODO doesn't trigger? { 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"; } { 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") ]; } { alias = "Grow light morning"; # TODO make configurable (snooze) trigger = [ (ha.trigger.at "09:00") ]; 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; } ]; } { alias = "Grow light night"; trigger = [ (ha.trigger.at "20:00") ]; action = [ (ha.action.off "switch.wakko") ]; } (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; } ]) (button "binary_sensor.bedroom_button_2" (ha.action.toggle "switch.wakko")) ]; }; }; 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; # oauth2-proxy NixOS module sets some non-standard headers, but we need # the preferred_username claim. auth_request_set $preferred_username $upstream_http_x_auth_request_preferred_username; proxy_set_header X-Forwarded-Preferred-Username $preferred_username; ''; }; # Duplicate relevant parts of root route to skip oauth2-proxy module magic. locations."/api/" = { proxyPass = "http://[::1]:8123"; proxyWebsockets = true; extraConfig = '' proxy_buffering off; auth_request 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"''; }; }; services.oauth2-proxy.nginx.virtualHosts = { "ha.mou.fo" = {}; }; }