From 57f8ce5803c60cc1a3ffc5bb63eb70aab0f63f25 Mon Sep 17 00:00:00 2001
From: matt1432 <matt@nelim.org>
Date: Mon, 16 Sep 2024 22:54:18 -0400
Subject: [PATCH] feat(hass): add voice command to set timers

---
 .../homie/modules/home-assistant/assist.nix   |   6 +
 .../modules/home-assistant/bluetooth.nix      |   1 +
 .../homie/modules/home-assistant/default.nix  |  18 +
 .../modules/home-assistant/docs/functions.nix |  90 +++
 .../homie/modules/home-assistant/docs/prompt  |  54 ++
 .../homie/modules/home-assistant/timer.nix    | 652 ++++++++++++++++++
 flake.lock                                    |  18 +-
 flake.nix                                     |   6 +-
 inputs.nix                                    |   6 +-
 legacyPackages/hass-components/default.nix    |   2 +-
 ...n.nix => extended-ollama-conversation.nix} |  14 +-
 11 files changed, 844 insertions(+), 23 deletions(-)
 create mode 100644 devices/homie/modules/home-assistant/docs/functions.nix
 create mode 100644 devices/homie/modules/home-assistant/docs/prompt
 create mode 100644 devices/homie/modules/home-assistant/timer.nix
 rename legacyPackages/hass-components/{extended-openai-conversation.nix => extended-ollama-conversation.nix} (59%)

diff --git a/devices/homie/modules/home-assistant/assist.nix b/devices/homie/modules/home-assistant/assist.nix
index f6e81130..9bfcf06b 100644
--- a/devices/homie/modules/home-assistant/assist.nix
+++ b/devices/homie/modules/home-assistant/assist.nix
@@ -1,4 +1,5 @@
 {
+  pkgs,
   self,
   wakewords-src,
   ...
@@ -10,10 +11,15 @@
 
   services = {
     home-assistant = {
+      customComponents = builtins.attrValues {
+        inherit (self.legacyPackages.${pkgs.system}.hass-components) extended-ollama-conversation;
+      };
+
       extraComponents = [
         "esphome"
         "ollama"
         "wyoming"
+        "scrape"
       ];
 
       config = {
diff --git a/devices/homie/modules/home-assistant/bluetooth.nix b/devices/homie/modules/home-assistant/bluetooth.nix
index c2f100e6..89ea1273 100644
--- a/devices/homie/modules/home-assistant/bluetooth.nix
+++ b/devices/homie/modules/home-assistant/bluetooth.nix
@@ -82,6 +82,7 @@
     "ibeacon"
     "led_ble"
     "kegtron"
+    "xiaomi_ble"
   ];
   services.mpd = {
     enable = true;
diff --git a/devices/homie/modules/home-assistant/default.nix b/devices/homie/modules/home-assistant/default.nix
index 51f724a6..2c4a74dd 100644
--- a/devices/homie/modules/home-assistant/default.nix
+++ b/devices/homie/modules/home-assistant/default.nix
@@ -4,6 +4,7 @@
     ./bluetooth.nix
     ./firmware.nix
     ./frontend.nix
+    ./timer.nix
   ];
 
   environment.systemPackages = [
@@ -23,6 +24,23 @@
         fi
       '';
     })
+
+    (pkgs.writeShellApplication {
+      name = "nix2yaml";
+      runtimeInputs = with pkgs; [remarshal];
+      text = ''
+        input="$1"
+        output="''${2:-""}"
+
+        yamlCode="$(nix eval --json --file "$input" | remarshal --if json --of yaml)"
+
+        if [[ "$output" != "" ]]; then
+            echo "$yamlCode" > "$output"
+        else
+            echo "$yamlCode"
+        fi
+      '';
+    })
   ];
 
   # TODO: some components / integrations / addons require manual interaction in the GUI, find way to make it all declarative
diff --git a/devices/homie/modules/home-assistant/docs/functions.nix b/devices/homie/modules/home-assistant/docs/functions.nix
new file mode 100644
index 00000000..18204569
--- /dev/null
+++ b/devices/homie/modules/home-assistant/docs/functions.nix
@@ -0,0 +1,90 @@
+# I use nix2yaml from ../default.nix to convert to to YAML and place it in the functions of extended_ollama_conversation
+[
+  {
+    function = {
+      name = "execute_service";
+      type = "native";
+    };
+
+    spec = {
+      name = "execute_services";
+      description = "Use this function to execute service of devices in Home Assistant.";
+
+      parameters = {
+        type = "object";
+
+        properties.list = {
+          type = "array";
+
+          items = {
+            type = "object";
+
+            properties = {
+              entity_id = {
+                description = "The entity_id retrieved from available devices. It must start with domain, followed by dot character.";
+                type = "string";
+              };
+
+              service = {
+                description = "The service to be called";
+                type = "string";
+              };
+            };
+
+            required = ["entity_id" "service"];
+          };
+        };
+      };
+    };
+  }
+
+  {
+    function = {
+      type = "script";
+
+      sequence = [
+        {
+          service = "script.assist_TimerStart";
+
+          data.duration = builtins.concatStringsSep "" [
+            ''{% if not hours %} {% set hours = "0" %} {% endif %}''
+            ''{% if not minutes %} {% set minutes = "0" %} {% endif %}''
+            ''{% if not seconds %} {% set seconds = "0" %} {% endif %}''
+
+            ''{{ hours | int(default=0) }}:{{ minutes | int(default=0) }}:{{ seconds | int(default=0) }}''
+          ];
+
+          target.entity_id = "timer.assist_timer1";
+        }
+      ];
+    };
+
+    spec = {
+      name = "timer_start";
+      description = "Use this function to start a timer in Home Assistant.";
+
+      parameters = {
+        type = "object";
+
+        properties = {
+          hours = {
+            type = "string";
+            description = "The amount of hours the timer should run for.";
+          };
+
+          minutes = {
+            type = "string";
+            description = "The amount of minutes the timer should run for.";
+          };
+
+          seconds = {
+            type = "string";
+            description = "The amount of seconds the timer should run for.";
+          };
+        };
+
+        required = [];
+      };
+    };
+  }
+]
diff --git a/devices/homie/modules/home-assistant/docs/prompt b/devices/homie/modules/home-assistant/docs/prompt
new file mode 100644
index 00000000..e5683c67
--- /dev/null
+++ b/devices/homie/modules/home-assistant/docs/prompt
@@ -0,0 +1,54 @@
+{%- set customize_glob_exposed_attributes = {
+  ".*": {
+    "friendly_name": true,
+    "temperature": true,
+    "current_temperature": true,
+    "temperature_unit": true,
+    "brightness": true,
+    "humidity": true,
+    "unit_of_measurement": true,
+    "device_class": true,
+    "current_position": true,
+    "percentage": true,
+    "volume_level": true,
+    "media_title": true,
+    "media_artist": true,
+    "media_album_name": true,
+  },
+} %}
+
+{%- macro get_exposed_attributes(entity_id) -%}
+  {%- set ns = namespace(exposed_attributes = {}, result = {}) %}
+  {%- for pattern, attributes in customize_glob_exposed_attributes.items() -%}
+    {%- if entity_id | regex_match(pattern) -%}
+      {%- set ns.exposed_attributes = dict(ns.exposed_attributes, **attributes) -%}
+    {%- endif -%}
+  {%- endfor -%}
+  {%- for attribute_key, should_include in ns.exposed_attributes.items() -%}
+    {%- if should_include and state_attr(entity_id, attribute_key) != None -%}
+      {%- set temp = {attribute_key: state_attr(entity_id, attribute_key)} if should_include is boolean else {attribute_key: should_include} -%}
+      {%- set ns.result = dict(ns.result, **temp) -%}
+    {%- endif -%}
+  {%- endfor -%}
+  {%- set result = ns.result | to_json if ns.result!={} else None -%}
+  {{"'" + result + "'" if result != None else ''}}
+{%- endmacro -%}
+
+I want you to act as smart home manager of Home Assistant.
+I will provide information of smart home along with a question, you will truthfully make correction or answer using information provided in one sentence in everyday language.
+
+Current Time: {{now()}}
+
+Available Devices:
+```csv
+entity_id,name,state,aliases,attributes
+{% for entity in exposed_entities -%}
+{{ entity.entity_id }},{{ entity.name }},{{ entity.state }},{{entity.aliases | join('/')}},{{get_exposed_attributes(entity.entity_id)}}
+{% endfor -%}
+```
+
+The current state of devices is provided in available devices.
+Use execute_services function to control devices.
+If needed, you can infer the entity_id from the name and common variations.
+Do not ask for confirmation to execute a service.
+Do not restate or appreciate what user says, rather make a quick inquiry.
diff --git a/devices/homie/modules/home-assistant/timer.nix b/devices/homie/modules/home-assistant/timer.nix
new file mode 100644
index 00000000..9265f134
--- /dev/null
+++ b/devices/homie/modules/home-assistant/timer.nix
@@ -0,0 +1,652 @@
+# From https://github.com/don86nl/ha_intents/blob/main/config/packages/assist_timers.yaml
+{...}: let
+  settings = {
+    timer_media_location = "/path/to/file.mp3";
+    timer_target = "kitchen";
+    timer_target_default = "media_player.music_player_daemon";
+    timer_tts = true;
+    timer_tts_message = "A set timer has finished.";
+    timer_tts_service = "tts.speak";
+    timer_tts_target = "tts.piper";
+    timer_volume = 0.4;
+  };
+in {
+  services.home-assistant = {
+    config = {
+      # TODO: format this properly
+      automation = [
+        {
+          action = [
+            {
+              alias = "Get generic variables from script";
+              variables = {
+                timer_media_location = "{{ settings.get('timer_media_location') }}";
+                timer_target = "{%- if settings.get('timer_target')[:13] == \"\" %} {{- settings.get('timer_target_default') }} {%- elif settings.get('timer_target')[:13] == \"media_player.\" %} {{- settings.get('timer_target') }} {%- elif (settings.get('timer_target')[:7] == \"sensor.\" or settings.get('timer_target')[:11] == \"input_text.\") and (states(settings.get('timer_target'))[:13] == \"media_player.\") %} {{- states(settings.get('timer_target')) }} {%- elif (settings.get('timer_target')[:7] == \"sensor.\" or settings.get('timer_target')[:11] == \"input_text.\") and (states(settings.get('timer_target')) == \"\") %} {{- settings.get('timer_target_default') }} {%- else %} {%- set media_player_list = states.media_player | map(attribute='entity_id') | list %} {%- if \"sensor.\" in settings.get('timer_target') or \"input_text.\" in target_area %} {%- set target_area = states(settings.get('timer_target')) %} {%- else %} {%- set target_area = settings.get('timer_target') %} {%- endif %}         {%- for entity_id in media_player_list %} {%- if area_name(entity_id) | lower == target_area | lower %} {{ entity_id }} {%- endif %} {%- endfor %} {%- endif %}  ";
+                timer_tts = "{{ settings.get('timer_tts') }}";
+                timer_tts_message = "{{ settings.get('timer_tts_message') }}";
+                timer_tts_service = "{{ settings.get('timer_tts_service') }}";
+                timer_tts_target = "{{ settings.get('timer_tts_target') }}";
+                timer_volume = "{{ settings.get('timer_volume') }}";
+              };
+            }
+            {
+              alias = "Store current device volume";
+              variables = {device_volume = "{{ state_attr(timer_target, 'volume_level') }}";};
+            }
+            {
+              alias = "Set volume for timer";
+              data = {volume_level = "{{ timer_volume }}";};
+              service = "media_player.volume_set";
+              target = {entity_id = "{{ timer_target }}";};
+            }
+            {
+              alias = "Media file or TTS";
+              choose = [
+                {
+                  alias = "Media file";
+                  conditions = [
+                    {
+                      alias = "Timer is a media file";
+                      condition = "template";
+                      value_template = "{{ timer_tts == false }}";
+                    }
+                  ];
+                  sequence = [
+                    {
+                      alias = "Play media";
+                      data = {
+                        announce = true;
+                        media_content_id = "{{ timer_media_location }}";
+                        media_content_type = "music";
+                      };
+                      enabled = true;
+                      service = "media_player.play_media";
+                      target = {entity_id = "{{ timer_target }}";};
+                    }
+                  ];
+                }
+              ];
+              default = [
+                {delay = {seconds = 1;};}
+                {
+                  alias = "Choose TTS service";
+                  choose = [
+                    {
+                      conditions = [
+                        {
+                          alias = "tts.cloud_say";
+                          condition = "template";
+                          value_template = "{{ timer_tts_service != 'tts.speak' }}";
+                        }
+                      ];
+                      sequence = [
+                        {
+                          data = {
+                            cache = true;
+                            entity_id = "{{ timer_target }}";
+                            message = "{% if timer_tts_message[:7] == \"sensor.\" or timer_tts_message[:11] == \"input_text.\" %} {{ states(timer_tts_message) }} {% else %} {{ timer_tts_message }} {% endif %}";
+                          };
+                          service = "{{ timer_tts_service }}";
+                        }
+                      ];
+                    }
+                  ];
+                  default = [
+                    {
+                      data = {
+                        cache = true;
+                        media_player_entity_id = "{{ timer_target }}";
+                        message = "{% if timer_tts_message[:7] == \"sensor.\" or timer_tts_message[:11] == \"input_text.\" %} {{ states(timer_tts_message) }} {% else %} {{ timer_tts_message }} {% endif %}";
+                      };
+                      service = "tts.speak";
+                      target = {entity_id = "{{ timer_tts_target }}";};
+                    }
+                  ];
+                }
+              ];
+            }
+            {
+              alias = "Restore device previous volume";
+              data = {volume_level = "{{ device_volume }}";};
+              service = "media_player.volume_set";
+              target = {entity_id = "{{ timer_target }}";};
+            }
+          ];
+          alias = "Assist - TimerReached";
+          condition = [
+            {
+              alias = "Finished timer is an assist timer";
+              condition = "template";
+              value_template = "{{ trigger.event.data.entity_id[:18] == 'timer.assist_timer' }}";
+            }
+          ];
+          description = "Assist automation when set timer time is reached.";
+          id = "assist_timerreached";
+          mode = "single";
+          trigger = [
+            {
+              alias = "Any timer reached";
+              event_type = "timer.finished";
+              id = "timer_finished";
+              platform = "event";
+            }
+          ];
+          variables = {
+            inherit settings;
+          };
+        }
+        {
+          action = [
+            {
+              alias = "Delay for Timer Reached automation";
+              delay = {seconds = 3;};
+            }
+            {
+              alias = "Reset timer location";
+              data = {value = "";};
+              service = "input_text.set_value";
+              target = {entity_id = "{{ 'input_text.' + trigger.entity_id[6:] + '_location' }}";};
+            }
+          ];
+          alias = "Assist - TimerFinished";
+          condition = [
+            {
+              alias = "Timer was active or paused";
+              condition = "template";
+              value_template = "{{ trigger.from_state != trigger.to_state }}";
+            }
+          ];
+          description = "Assist automation when set timer time is finished.";
+          id = "assist_timerfinished";
+          mode = "parallel";
+          trigger = [
+            {
+              alias = "Assist timer finished or cancelled";
+              entity_id = ["timer.assist_timer1" "timer.assist_timer2" "timer.assist_timer3"];
+              platform = "state";
+              to = "idle";
+            }
+          ];
+        }
+      ];
+      homeassistant = {
+        customize = {
+          "script.assist_timerstart" = {
+            inherit settings;
+          };
+        };
+      };
+      input_text = {
+        assist_timer1_location = {
+          icon = "mdi:assistant";
+          max = 255;
+          name = "Assist - Timer 1 Location";
+        };
+        assist_timer2_location = {
+          icon = "mdi:assistant";
+          max = 255;
+          name = "Assist - Timer 2 Location";
+        };
+        assist_timer3_location = {
+          icon = "mdi:assistant";
+          max = 255;
+          name = "Assist - Timer 3 Location";
+        };
+      };
+      intent_script = {
+        TimerDuration = {
+          action = [{stop = "";}];
+          async_action = true;
+        };
+        TimerPause = {
+          action = [
+            {
+              data = {
+                entity_id = "{{ entity_id }}";
+                timer_action = "{{ timer_action }}";
+              };
+              service = "script.assist_TimerPause";
+            }
+          ];
+          async_action = true;
+        };
+        TimerStart = {
+          action = [
+            {
+              data = {duration = "{{hours | int(default=0)}}:{{ minutes | int(default=0) }}:{{ seconds | int(default=0) }}";};
+              service = "script.assist_TimerStart";
+            }
+          ];
+          async_action = false;
+        };
+        TimerStop = {
+          action = [
+            {
+              data = {entity_id = "{{ entity_id }}";};
+              service = "script.assist_TimerStop";
+            }
+          ];
+          async_action = true;
+        };
+      };
+      script = {
+        assist_timerpause = {
+          alias = "Assist - TimerPause";
+          description = "Script for pausing a timer using HA Assist.";
+          icon = "mdi:assistant";
+          mode = "single";
+          sequence = [
+            {
+              choose = [
+                {
+                  conditions = [
+                    {
+                      alias = "Single Timer";
+                      condition = "template";
+                      value_template = "{{ entity_id[:18] == 'timer.assist_timer' }}";
+                    }
+                  ];
+                  sequence = [
+                    {
+                      alias = "Single timer: Idle or active";
+                      choose = [
+                        {
+                          conditions = [
+                            {
+                              alias = "Timer not active";
+                              condition = "template";
+                              value_template = "{{ states(entity_id) == 'idle' }}";
+                            }
+                          ];
+                          sequence = [{stop = "Timer is not active";}];
+                        }
+                      ];
+                      default = [
+                        {
+                          alias = "Pause or resume";
+                          choose = [
+                            {
+                              alias = "Pause";
+                              conditions = [
+                                {
+                                  alias = "Action = pause";
+                                  condition = "template";
+                                  value_template = "{{ timer_action == 'pause' }}";
+                                }
+                              ];
+                              sequence = [
+                                {
+                                  alias = "Pause timer";
+                                  service = "timer.pause";
+                                  target = {entity_id = "{{ entity_id }}";};
+                                }
+                                {stop = "Pause timer";}
+                              ];
+                            }
+                          ];
+                          default = [
+                            {
+                              alias = "Resume timer";
+                              service = "timer.start";
+                              target = {entity_id = "{{ entity_id }}";};
+                            }
+                            {stop = "Resume timer";}
+                          ];
+                        }
+                      ];
+                    }
+                  ];
+                }
+                {
+                  alias = "No specific timer";
+                  conditions = [
+                    {
+                      alias = "No specific Timer";
+                      condition = "template";
+                      value_template = "{{ entity_id == 'null' }}";
+                    }
+                    {
+                      alias = "Timer(s) are active";
+                      condition = "template";
+                      value_template = "{{ states.timer \n   | rejectattr('state','eq','idle') \n   | selectattr('entity_id','match','timer.assist_timer*')\n   | map(attribute='entity_id') \n   | list\n   | length > 0 }}";
+                    }
+                  ];
+                  sequence = [
+                    {
+                      alias = "No specific timer: # active?";
+                      choose = [
+                        {
+                          conditions = [
+                            {
+                              alias = "No specific timer asked";
+                              condition = "template";
+                              value_template = "{{ entity_id == 'null' }}";
+                            }
+                            {
+                              alias = "Multiple timers active";
+                              condition = "template";
+                              value_template = "{{ states.timer \n   | rejectattr('state','eq','idle') \n   | selectattr('entity_id','match','timer.assist_timer*')\n   | map(attribute='entity_id') \n   | list\n   | length > 1 }}";
+                            }
+                          ];
+                          sequence = [{stop = "Multiple timers active, none specified";}];
+                        }
+                      ];
+                      default = [
+                        {
+                          alias = "Pause or resume";
+                          choose = [
+                            {
+                              alias = "Pause";
+                              conditions = [
+                                {
+                                  alias = "Action = pause";
+                                  condition = "template";
+                                  value_template = "{{ timer_action == 'pause' }}";
+                                }
+                              ];
+                              sequence = [
+                                {
+                                  alias = "Pause timer";
+                                  service = "timer.pause";
+                                  target = {entity_id = "{{ states.timer \n | rejectattr('state','eq','idle') \n | selectattr('entity_id','match','timer.assist_timer*')\n | map(attribute='entity_id') \n | join(', ') }}";};
+                                }
+                                {stop = "Pause timer";}
+                              ];
+                            }
+                          ];
+                          default = [
+                            {
+                              alias = "Resume timer";
+                              service = "timer.start";
+                              target = {entity_id = "{{ states.timer \n | rejectattr('state','eq','idle') \n | selectattr('entity_id','match','timer.assist_timer*')\n | map(attribute='entity_id') \n | join(', ') }}";};
+                            }
+                            {stop = "Resume timer";}
+                          ];
+                        }
+                      ];
+                    }
+                  ];
+                }
+                {
+                  alias = "All timers";
+                  conditions = [
+                    {
+                      alias = "All timers";
+                      condition = "template";
+                      value_template = "{{ entity_id == 'all' }}";
+                    }
+                  ];
+                  sequence = [
+                    {
+                      alias = "Timers active?";
+                      choose = [
+                        {
+                          alias = "No timers active";
+                          conditions = [
+                            {
+                              alias = "No timers active";
+                              condition = "template";
+                              value_template = "{{ states.timer \n   | rejectattr('state','eq','idle') \n   | selectattr('entity_id','match','timer.assist_timer*')\n   | map(attribute='entity_id') \n   | list\n   | length == 0 }}";
+                            }
+                          ];
+                          sequence = [{stop = "No timers active";}];
+                        }
+                      ];
+                      default = [
+                        {
+                          alias = "Pause or resume";
+                          choose = [
+                            {
+                              alias = "Pause";
+                              conditions = [
+                                {
+                                  alias = "Action = pause";
+                                  condition = "template";
+                                  value_template = "{{ timer_action == 'pause' }}";
+                                }
+                              ];
+                              sequence = [
+                                {
+                                  alias = "Pause timer";
+                                  service = "timer.pause";
+                                  target = {entity_id = "{{ states.timer \n | rejectattr('state','eq','idle') \n | selectattr('entity_id','match','timer.assist_timer*')\n | map(attribute='entity_id') \n | join(', ') }}";};
+                                }
+                                {stop = "Pause timer";}
+                              ];
+                            }
+                          ];
+                          default = [
+                            {
+                              alias = "Resume timer";
+                              service = "timer.start";
+                              target = {entity_id = "{{ states.timer \n | rejectattr('state','eq','idle') \n | selectattr('entity_id','match','timer.assist_timer*')\n | map(attribute='entity_id') \n | join(', ') }}";};
+                            }
+                            {stop = "Resume timer";}
+                          ];
+                        }
+                      ];
+                    }
+                  ];
+                }
+              ];
+            }
+          ];
+          variables = {
+            entity_id = "{% if entity_id is set or entity_id != \"\" %} {{ entity_id }} {% else %} null {% endif %}";
+            timer_action = "{% if timer_action is set or timer_action != \"\" %} {{ timer_action }} {% else %} resume {% endif %}";
+          };
+        };
+        assist_timerstart = {
+          alias = "Assist - TimerStart";
+          description = "Script for starting a timer using HA Assist.";
+          icon = "mdi:assistant";
+          mode = "single";
+          sequence = [
+            {
+              alias = "Set variables";
+              variables = {timer_location = "{%- if settings.get('timer_target')[:13] == \"media_player.\" %} {{ area_name(settings.get('timer_target')) | lower }} {% elif (settings.get('timer_target')[:7] == \"sensor.\" or settings.get('timer_target')[:11] == \"input_text.\") and states(settings.get('timer_target'))[:13] == \"media_player.\" %} {{- states(settings.get('timer_target')) }} {%- elif settings.get('timer_target')[:13] != \"media_player.\" and settings.get('timer_target')[:7] != \"sensor.\" and settings.get('timer_target')[:11] != \"input_text.\" %} {{- settings.get('timer_target') }} {%- elif (settings.get('timer_target')[:7] == \"sensor.\" or settings.get('timer_target')[:11] == \"input_text.\") and (states(settings.get('timer_target')) != \"\") and (states(settings.get('timer_target'))[:13] == \"media_player.\") %} {{ area_name(settings.get('timer_target_default')) }} {%- elif (settings.get('timer_target')[:7] == \"sensor.\" or settings.get('timer_target')[:11] == \"input_text.\") %} {% if states(settings.get('timer_target')) != \"\" and states(settings.get('timer_target')) != \"not_home\" and states(settings.get('timer_target')) != 0 %} {{ states(settings.get('timer_target')) }} {% else %} {{- area_name(settings.get('timer_target_default')) | lower }} {%- endif %} {%- else %} {{- area_name(settings.get('timer_target')) | lower }} {%- endif %}";};
+            }
+            {
+              alias = "Set timer location";
+              data = {value = "{{ timer_location }}";};
+              service = "input_text.set_value";
+              target = {entity_id = "{% if states('timer.assist_timer1') != 'active' and states('timer.assist_timer1') != 'paused' %} input_text.assist_timer1_location {% elif states('timer.assist_timer2') != 'active' and states('timer.assist_timer2') != 'paused' %} input_text.assist_timer2_location {% else %} input_text.assist_timer3_location {% endif%}";};
+            }
+            {
+              alias = "Start timer";
+              data_template = {duration = "{{ duration }}";};
+              service = "timer.start";
+              target = {entity_id = "{% if states('timer.assist_timer1') != 'active' and states('timer.assist_timer1') != 'paused' %} timer.assist_timer1 {% elif states('timer.assist_timer2') != 'active' and states('timer.assist_timer2') != 'paused' %} timer.assist_timer2 {% else %} timer.assist_timer3{% endif%}";};
+            }
+          ];
+          variables = {
+            inherit settings;
+          };
+        };
+        assist_timerstop = {
+          alias = "Assist - TimerStop";
+          description = "Script for stopping a timer using HA Assist.";
+          icon = "mdi:assistant";
+          mode = "single";
+          sequence = [
+            {
+              alias = "Set variables";
+              variables = {entity_id = "{% if entity_id is set or entity_id != \"\" %} {{ entity_id }} {% else %} null {% endif %}";};
+            }
+            {
+              choose = [
+                {
+                  alias = "Stop Timer music";
+                  conditions = [
+                    {
+                      alias = "Timer is a media file";
+                      condition = "template";
+                      value_template = "{{ timer_tts == false }}";
+                    }
+                    {
+                      condition = "template";
+                      value_template = "{% set mediaplayer = namespace(entity=[]) %}\n{% for player in states.media_player %}\n  {%- if ((state_attr(player.entity_id, 'media_content_id') |lower != 'none' and state_attr(player.entity_id, 'media_content_id')[:47][38:] == 'timer.mp3') or state_attr(player.entity_id, 'media_title') | lower == 'timer') and states(player.entity_id) == 'playing' -%}\n    {%- set mediaplayer.entity = player.entity_id -%}\n  {% endif -%}\n{% endfor %}\n{{ mediaplayer.entity[:12] == 'media_player' }}";
+                    }
+                  ];
+                  sequence = [
+                    {
+                      alias = "Stop timer music";
+                      service = "media_player.media_stop";
+                      target = {entity_id = "{% set mediaplayer = namespace(entity=[]) %} {% for player in states.media_player %}\n  {% if ((state_attr(player.entity_id, 'media_content_id') |lower != 'none' and state_attr(player.entity_id, 'media_content_id')[:47][38:] == 'timer.mp3') or state_attr(player.entity_id, 'media_title') | lower == 'timer') and states(player.entity_id) == 'playing' %}\n    {% set mediaplayer.entity = player.entity_id %}\n  {% endif %}\n{% endfor %} {{ mediaplayer.entity }}";};
+                    }
+                  ];
+                }
+                {
+                  conditions = [
+                    {
+                      alias = "Single Timer";
+                      condition = "template";
+                      value_template = "{{ entity_id[:18] == 'timer.assist_timer' }}";
+                    }
+                  ];
+                  sequence = [
+                    {
+                      alias = "Single timer: Idle or active";
+                      choose = [
+                        {
+                          conditions = [
+                            {
+                              alias = "Timer not active";
+                              condition = "template";
+                              value_template = "{{ states(entity_id) == 'idle' }}";
+                            }
+                          ];
+                          sequence = [{stop = "Timer is not active";}];
+                        }
+                      ];
+                      default = [
+                        {
+                          alias = "Cancel single timer";
+                          service = "timer.cancel";
+                          target = {entity_id = "{{ entity_id }}";};
+                        }
+                        {
+                          alias = "Reset timer location value";
+                          data = {value = "0";};
+                          service = "input_text.set_value";
+                          target = {entity_id = "{{ states.timer \n   | selectattr('state','eq','active') \n   | selectattr('entity_id','match','timer.assist_timer*')\n   | map(attribute='entity_id') \n   | join('_location, ') | replace('timer.', 'input_text.') }}";};
+                        }
+                        {stop = "Timer cancelled";}
+                      ];
+                    }
+                  ];
+                }
+                {
+                  alias = "No specific timer";
+                  conditions = [
+                    {
+                      alias = "No specific Timer";
+                      condition = "template";
+                      value_template = "{{ entity_id == 'null' }}";
+                    }
+                    {
+                      alias = "Timer(s) are active";
+                      condition = "template";
+                      value_template = "{{ states.timer \n   | rejectattr('state','eq','idle') \n   | selectattr('entity_id','match','timer.assist_timer*')\n   | map(attribute='entity_id') \n   | list\n   | length > 0 }}";
+                    }
+                  ];
+                  sequence = [
+                    {
+                      alias = "No specific timer: # active?";
+                      choose = [
+                        {
+                          conditions = [
+                            {
+                              alias = "No specific timer asked";
+                              condition = "template";
+                              value_template = "{{ entity_id == 'null' }}";
+                            }
+                            {
+                              alias = "Multiple timers active";
+                              condition = "template";
+                              value_template = "{{ states.timer \n   | rejectattr('state','eq','idle') \n   | selectattr('entity_id','match','timer.assist_timer*')\n   | map(attribute='entity_id') \n   | list\n   | length > 1 }}";
+                            }
+                          ];
+                          sequence = [{stop = "Multiple timers active, none specified";}];
+                        }
+                      ];
+                      default = [
+                        {
+                          alias = "Cancel single timer";
+                          service = "timer.cancel";
+                          target = {entity_id = "{{ states.timer \n | rejectattr('state','eq','idle') \n | selectattr('entity_id','match','timer.assist_timer*')\n | map(attribute='entity_id') \n | join(', ') }}";};
+                        }
+                        {
+                          alias = "Reset timer location value";
+                          data = {value = "0";};
+                          metadata = {};
+                          service = "input_text.set_value";
+                          target = {entity_id = "{{ states.timer \n   | rejectattr('state','eq','idle') \n   | selectattr('entity_id','match','timer.assist_timer*')\n   | map(attribute='entity_id') \n   | join('_location, ') | replace('timer.', 'input_text.') }}";};
+                        }
+                        {stop = "Timer cancelled";}
+                      ];
+                    }
+                  ];
+                }
+                {
+                  alias = "All timers";
+                  conditions = [
+                    {
+                      alias = "All timers";
+                      condition = "template";
+                      value_template = "{{ entity_id == 'all' }}";
+                    }
+                  ];
+                  sequence = [
+                    {
+                      alias = "Timers active?";
+                      choose = [
+                        {
+                          alias = "No timers active";
+                          conditions = [
+                            {
+                              alias = "No timers active";
+                              condition = "template";
+                              value_template = "{{ states.timer \n   | rejectattr('state','eq','idle') \n   | selectattr('entity_id','match','timer.assist_timer*')\n   | map(attribute='entity_id') \n   | list\n   | length == 0 }}";
+                            }
+                          ];
+                          sequence = [{stop = "No timers active";}];
+                        }
+                      ];
+                      default = [
+                        {
+                          alias = "Cancel all timers";
+                          service = "timer.cancel";
+                          target = {entity_id = "{{ states.timer \n | rejectattr('state','eq','idle') \n | selectattr('entity_id','match','timer.assist_timer*')\n | map(attribute='entity_id') \n | join(', ') }}";};
+                        }
+                        {stop = "Cancel all timers";}
+                      ];
+                    }
+                  ];
+                }
+              ];
+            }
+          ];
+          variables = {entity_id = "{% if entity_id is set or entity_id != \"\" %} {{ entity_id }} {% else %} null {% endif %}";};
+        };
+      };
+      timer = {
+        assist_timer1 = {
+          icon = "mdi:assistant";
+          name = "Assist - Timer 1";
+          restore = true;
+        };
+        assist_timer2 = {
+          icon = "mdi:assistant";
+          name = "Assist - Timer 2";
+          restore = true;
+        };
+        assist_timer3 = {
+          icon = "mdi:assistant";
+          name = "Assist - Timer 3";
+          restore = true;
+        };
+      };
+    };
+  };
+}
diff --git a/flake.lock b/flake.lock
index fb8f70b1..8bdcccb8 100644
--- a/flake.lock
+++ b/flake.lock
@@ -233,19 +233,19 @@
         "type": "github"
       }
     },
-    "extended-openai-conversation-src": {
+    "extended-ollama-conversation-src": {
       "flake": false,
       "locked": {
-        "lastModified": 1708531177,
-        "narHash": "sha256-BwBroYcPQX3pv4iFR1ynmC5xQRTVAFAsOGfDGyXkES4=",
-        "owner": "jekalmin",
-        "repo": "extended_openai_conversation",
-        "rev": "1b20b56e81e5e5067b72a2ba2c8f51dd0a73eef1",
+        "lastModified": 1722207930,
+        "narHash": "sha256-WF54xX9DF8QJf7kZtsDyCSZRR6ktm6gNpI7WqKSIjZY=",
+        "owner": "TheNimaj",
+        "repo": "extended_ollama_conversation",
+        "rev": "d8ea4190e75b9f8127cd55403e775dd47dd2b79b",
         "type": "github"
       },
       "original": {
-        "owner": "jekalmin",
-        "repo": "extended_openai_conversation",
+        "owner": "TheNimaj",
+        "repo": "extended_ollama_conversation",
         "type": "github"
       }
     },
@@ -1638,7 +1638,7 @@
         "dracul-ha-src": "dracul-ha-src",
         "dracula-plymouth-src": "dracula-plymouth-src",
         "eisa-scripts-src": "eisa-scripts-src",
-        "extended-openai-conversation-src": "extended-openai-conversation-src",
+        "extended-ollama-conversation-src": "extended-ollama-conversation-src",
         "firefox-gx-src": "firefox-gx-src",
         "flake-utils": "flake-utils",
         "flakegen": "flakegen",
diff --git a/flake.nix b/flake.nix
index 9056925a..1d510cf0 100644
--- a/flake.nix
+++ b/flake.nix
@@ -58,10 +58,10 @@
       repo = "mpv-scripts";
       type = "github";
     };
-    extended-openai-conversation-src = {
+    extended-ollama-conversation-src = {
       flake = false;
-      owner = "jekalmin";
-      repo = "extended_openai_conversation";
+      owner = "TheNimaj";
+      repo = "extended_ollama_conversation";
       type = "github";
     };
     firefox-gx-src = {
diff --git a/inputs.nix b/inputs.nix
index d70cf112..8b438fe3 100644
--- a/inputs.nix
+++ b/inputs.nix
@@ -168,9 +168,9 @@ let
       repo = "home-llm";
     }
     {
-      name = "extended-openai-conversation-src";
-      owner = "jekalmin";
-      repo = "extended_openai_conversation";
+      name = "extended-ollama-conversation-src";
+      owner = "TheNimaj";
+      repo = "extended_ollama_conversation";
     }
 
     {
diff --git a/legacyPackages/hass-components/default.nix b/legacyPackages/hass-components/default.nix
index 9b3c3027..9097f730 100644
--- a/legacyPackages/hass-components/default.nix
+++ b/legacyPackages/hass-components/default.nix
@@ -4,5 +4,5 @@ pkgs.lib.makeScope pkgs.newScope (hass: let
     hass.callPackage file (inputs // {});
 in {
   home-llm = buildHassComponent ./home-llm.nix;
-  extended-openai-conversation = buildHassComponent ./extended-openai-conversation.nix;
+  extended-ollama-conversation = buildHassComponent ./extended-ollama-conversation.nix;
 })
diff --git a/legacyPackages/hass-components/extended-openai-conversation.nix b/legacyPackages/hass-components/extended-ollama-conversation.nix
similarity index 59%
rename from legacyPackages/hass-components/extended-openai-conversation.nix
rename to legacyPackages/hass-components/extended-ollama-conversation.nix
index 67c8bd73..7046404e 100644
--- a/legacyPackages/hass-components/extended-openai-conversation.nix
+++ b/legacyPackages/hass-components/extended-ollama-conversation.nix
@@ -1,15 +1,15 @@
 {
-  extended-openai-conversation-src,
+  extended-ollama-conversation-src,
   buildHomeAssistantComponent,
   fetchFromGitHub,
-  python312Packages,
+  python3Packages,
   ...
 }: let
   inherit (builtins) fromJSON readFile;
 
-  manifest = fromJSON (readFile "${extended-openai-conversation-src}/custom_components/extended_openai_conversation/manifest.json");
+  manifest = fromJSON (readFile "${extended-ollama-conversation-src}/custom_components/extended_ollama_conversation/manifest.json");
 
-  openai = python312Packages.openai.overrideAttrs (o: rec {
+  openai = python3Packages.openai.overrideAttrs (o: rec {
     version = "1.3.8";
 
     src = fetchFromGitHub {
@@ -23,11 +23,11 @@
   });
 in
   buildHomeAssistantComponent {
-    owner = "jekalmin";
+    owner = "TheNimaj";
 
     inherit (manifest) domain version;
 
-    src = extended-openai-conversation-src;
+    src = extended-ollama-conversation-src;
 
-    propagatedBuildInputs = [openai];
+    propagatedBuildInputs = [python3Packages.ollama openai];
   }