Semi-automatic water parameters testing

From this:

To this:

The process is the following:

  • Take a sample of the water using the test strip;
  • Take a photo of the strip and the reference values on the packing;
  • Upload the photo to Home Assistant directly from the phone;
  • The photo gets sent to an LLM engine for analysis;
  • Parsing the results and feeding some sensors with it.

Integrations used:

  • Folder watcher - fires up and event every time a new file appears in a specific folder. This is the trigger for the automation.
  • LLM Vision - Sends the image to an LLM engine for analysis.
  • MQTT - I find it easy to create a manual MQTT sensor in HA and the use MQTT Publish to feed it with data. The short text status is the sensor value. The actual measurements are fed into the sensor as attributes.
  • Template sensors - To extract the attributes into separate sensors.

MQTT sensor definition:

mqtt:
    sensor:
      - name: "Параметри на водата"
        unique_id: aquarium_water_parameters
        state_topic: "homeassistant/sensor/aquarium/water/state"
        json_attributes_topic: "homeassistant/sensor/aquarium/water/attributes"

The automation:

alias: Анализа на параметрите на водата (LLM)
description: ""
triggers:
  - trigger: event.received
    target:
      device_id: 68e0feb5601fc143a309e991b8ed83de
    options:
      event_type:
        - closed
    enabled: false
  - trigger: event
    event_type: folder_watcher
    event_data:
      event_type: closed
conditions: []
actions:
  - action: llmvision.image_analyzer
    metadata: {}
    data:
      store_in_timeline: true
      use_memory: false
      include_filename: true
      target_width: 1280
      max_tokens: 500
      generate_title: true
      expose_images: false
      response_format: json
      title_field: title
      description_field: description
      provider: 01KCQCZEDGQCB9N3AGNJE26672
      message: |2-
            Aquarium water parameters analysis. The photo has the legend and the sample
            strip. The water is generaly hard and alkaline. That is just the way it is.
            The tank has 6 neon tetras, 3-5 cherry shrimps, some plants, snails and
            driftwood. Respod as a JSON structured water parameter data. Include a text
            field containing a narrative description of the water condition in Macedonian. Do not use
            ranges in the response data. Make your best guess.
      image_file: "{{ trigger.event.data.path }}"
      structure: "{\n  \"type\": \"object\",\n  \"properties\": {\n    \"title\": {\n      \"type\": \"string\",\n      \"description\": \"Event title\"\n    },\n    \"text\": {\n      \"type\": \"string\",\n      \"description\": \"Water quality description\"\n    },\n    \"ph\": {\n      \"type\": \"number\"\n    },\n    \"total_alkalinity_mg_l\": {\n      \"type\": \"number\"\n    },\n    \"general_hardness_mg_l\": {\n      \"type\": \"number\"\n    },\n    \"total_chlorine_mg_l\": {\n      \"type\": \"number\"\n    },\n    \"free_chlorine_mg_l\": {\n      \"type\": \"number\"\n    },\n    \"copper_mg_l\": {\n      \"type\": \"number\"\n    },\n    \"iron_mg_l\": {\n      \"type\": \"number\"\n    },\n    \"nitrate_mg_l\": {\n      \"type\": \"number\"\n    },\n    \"nitrite_mg_l\": {\n      \"type\": \"number\"\n    }\n  },\n  \"required\": [\n    \"title\",\n    \"text\",\n    \"ph\",\n    \"total_alkalinity_mg_l\",\n\t\"general_hardness_mg_l\",\n\t\"total_chlorine_mg_l\",\n    \"free_chlorine_mg_l\",\n    \"copper_mg_l\",\n    \"iron_mg_l\",\n    \"nitrate_mg_l\",\n    \"nitrite_mg_l\"\n  ],\n  \"additionalProperties\": false\n}"
    response_variable: response_data
  - choose:
      - conditions:
          - condition: template
            value_template: "{{ response_data.response_text is defined }}"
        sequence:
          - action: mqtt.publish
            metadata: {}
            data:
              retain: true
              topic: homeassistant/sensor/aquarium/water/state
              payload: "{{ response_data.response_text }}"
              qos: "0"
    default:
      - action: mqtt.publish
        metadata: {}
        data:
          retain: true
          topic: homeassistant/sensor/aquarium/water/state
          payload: "{{ response_data.structured_response.text }}"
          qos: "0"
      - action: mqtt.publish
        metadata: {}
        data:
          retain: true
          topic: homeassistant/sensor/aquarium/water/attributes
          payload: |-
            { 
              "Text": "{{ response_data.structured_response.text }}",
              "ph": "{{ response_data.structured_response.ph }}",
              "total_alkalinity_mg_l": "{{ response_data.structured_response.total_alkalinity_mg_l }}",
              "general_hardness_mg_l": "{{ response_data.structured_response.general_hardness_mg_l }}",
              "total_chlorine_mg_l": "{{ response_data.structured_response.total_chlorine_mg_l }}",
              "free_chlorine_mg_l": "{{ response_data.structured_response.free_chlorine_mg_l }}",
              "copper_mg_l": "{{ response_data.structured_response.copper_mg_l }}",
              "iron_mg_l": "{{ response_data.structured_response.iron_mg_l }}",
              "nitrate_mg_l": "{{ response_data.structured_response.nitrate_mg_l }}",
              "nitrite_mg_l": "{{ response_data.structured_response.nitrite_mg_l }}"
            }
              
          qos: "0"
        enabled: true
mode: queued
max: 10

Notes:

  • The folder watcher fires up several event on file upload. That last one in the sequence has event_type: closed. This is the one we want.
  • There are two trigger clauses in the automation, but only one is enabled. Started this project using event.received but this ended being unreliable. Events would fire, but the automation wouldn’t tigger. I moved to older event type trigger and everything works now. I might need some work in the future is i add more folder watchers.
  • There is a condition in the automation that checks for existence of response_data.response_text in the result. This means that llmvision.image_analyzer returned an error. This error is then populated into the sensor as value. Otherwise, parse the result and populate the sensor and the attributes with the response data.