在Home Assistant中利用動態圖片來呈現魚缸水位的變化

簡單地講,這是把魚缸的水位用一組示意圖來表示,圖中藍色的水量會隨著水位的值而變動。例如,中間這個是一個背濾槽示意圖,總共隔了三格,由左到右分別為入水格、濾材格與馬達格,最右邊馬達格的水位,現在落在上下限之間。

這是一個不做覺得可惜但做了又覺得沒用的東西,主因是沒這麼直覺好讀。

先從頭講起,我的魚缸總共有設置了四個水位的監測

  • 主缸:一顆浮球開關,正常情況為低水位,若在高水位會有背濾白棉堵塞且嚴重時水會溢出缸外的風險。
  • 背濾的濾材格:一顆浮球開關,正常情況為高水位,若在低水位時水位無法完全覆蓋陶瓷環。
  • 背濾的馬達格:二顆浮球開關組成低中高水位(*1),正常情況為中水位,水位過高可能是水加太多或馬達失效(*2),水位過低會有馬達空轉的風險。
  • 補水桶:一顆非接觸式液體感應器,正常情況為高水位,若在低水位時避免補水馬達空轉,不會執行自動補水。

*1:馬達格由二顆浮球開關組成一個水位sensor,當二個浮球皆下沉時為低水位0,當下面的浮球上浮及上面的浮球下沉時為中水位1,當二個浮球皆上浮為高水位2。

*2:背濾的所有水量都在魚缸內,馬達格在馬達開啟時水會一直打往主缸,若水位過高只有水加太多或馬達失效這二種可能性,不會有水溢出缸外的風險。但如果是底濾缸,馬達失效或關閉時,主缸裡超出溢流板的水會退回到底濾缸中,有可能造成水溢出底濾缸外,所以馬達格的水位要保持在一個上下水位之間。

這些浮球開關和非接觸式液體感應器都接在ESP32上,並透過MQTT回報。非接觸式液體感應器在前文在ESP32上實作XKC-Y25-V非接觸式液體偵測器裡有提到過,浮球開關的部分,浮球開關有二條線,一條接在GND上,另一條選任意一個GPIO即可。最簡單的程式如下

#define floatSwitchPin 5 // 浮球開關接在GPIO 5

void setup() 
{
  Serial.begin(115200);
  pinMode(floatSwitchPin, INPUT_PULLUP);
}

void loop()
{
  Serial.println(digitalRead(floatSwitchPin)); 
  delay(1000);
}

在Home Assistant中,建立四個MQTT sensor來接收四個位置的水位讀值

sensor:
  - name: Aquarium_MainTank_WaterLevel
    state_topic: "aquarium/maintank/water_level"
    value_template: "{{ value }}"
  - name: Aquarium_Bucket_WaterLevel
    state_topic: "aquarium/bucket/water_level"
    value_template: "{{ value }}"
  - name: Aquarium_FilterTank_WaterLevel
    state_topic: "aquarium/filtertank/water_level"
    value_template: "{{ value }}"
  - name: Aquarium_PumpTank_WaterLevel
    state_topic: "aquarium/pumptank/water_level"
    value_template: "{{ value }}"

但這樣做還不太夠,因為當水位在浮球開關的高低之間時,微小的水面擾動可能就會改變sensor的值,造成sensor的回報會很頻繁,若有寫水位警報的automation就會看到警報一直在發佈。改善的方式可以利用automation去做改寫,將sensor的值改寫成input_number來表示水位的高低。

建立input_number,馬達格因為有二個浮球開關所以會有0(低)、0.5(中低之間)、1(中)、1.5(中高之間)與2(高)五種水位,其他只有0(低)、1(高低之間)與2(高)三種。此外,多留一個最小值-1做程式的除錯用。

aquarium_bucket_waterlevel:
  name: Aquarium_Bucket_Waterlevel
  min: -1
  max: 2
aquarium_maintank_waterlevel:
  name: Aquarium_Maintank_Waterlevel
  min: -1
  max: 2
aquarium_filtertank_waterlevel:
  name: Aquarium_Filtertank_Waterlevel
  min: -1
  max: 2
aquarium_pumptank_waterlevel:
  name: Aquarium_Pumptank_Waterlevel
  min: -1
  max: 2

Automation的部分,分成穩定水位與非穩定水位二種。當sensor的值改變時,認定是非穩定水位狀態

- alias: Aquarium:Water_Level_Rendering(Varied)
  trigger:
    - platform: state
      entity_id:
        - sensor.aquarium_bucket_waterlevel
        - sensor.aquarium_maintank_waterlevel
        - sensor.aquarium_filtertank_waterlevel
        - sensor.aquarium_pumptank_waterlevel
  condition:
    - condition: template
      value_template: "{{ trigger.from_state.state != 'unavailable' and trigger.to_state.state != 'unavailable' and trigger.from_state.state != trigger.to_state.state }}"
  action:
    - service: input_number.set_value
      target:
        entity_id: >
          {% if trigger.entity_id == "sensor.aquarium_bucket_waterlevel" %} input_number.aquarium_bucket_waterlevel
          {% elif trigger.entity_id == "sensor.aquarium_maintank_waterlevel" %} input_number.aquarium_maintank_waterlevel
          {% elif trigger.entity_id == "sensor.aquarium_filtertank_waterlevel" %} input_number.aquarium_filtertank_waterlevel
          {% elif trigger.entity_id == "sensor.aquarium_pumptank_waterlevel" %} input_number.aquarium_pumptank_waterlevel
          {% else %} input_number.dummy_number
          {% endif %}
      data:
        value: >
          {% if trigger.entity_id == "sensor.aquarium_pumptank_waterlevel" %}
            {% if trigger.from_state.state == '1' and trigger.to_state.state == '2' %} 1.5
            {% elif trigger.from_state.state == '2' and trigger.to_state.state == '1' %} 1.5
            {% elif trigger.from_state.state == '0' and trigger.to_state.state == '1' %} 0.5
            {% elif trigger.from_state.state == '1' and trigger.to_state.state == '0' %} 0.5
            {% else %} -1
            {% endif %}
          {% else %}
            1
          {% endif %}
  mode: parallel
  max: 30

當sensor的值改變且持續了三分鐘,就當做是穩定水位狀態。

- alias: Aquarium:Water_Level_Rendering(Stable)
  trigger:
    - platform: state
      entity_id:
        - sensor.aquarium_bucket_waterlevel
        - sensor.aquarium_maintank_waterlevel
        - sensor.aquarium_filtertank_waterlevel
        - sensor.aquarium_pumptank_waterlevel
      for:
        minutes: 3
  condition:
    - condition: template
      value_template: "{{ trigger.from_state.state != 'unavailable' and trigger.to_state.state != 'unavailable' and trigger.from_state.state != trigger.to_state.state }}"
  action:
    - service: input_number.set_value
      target:
        entity_id: >
          {% if trigger.entity_id == "sensor.aquarium_bucket_waterlevel" %} input_number.aquarium_bucket_waterlevel
          {% elif trigger.entity_id == "sensor.aquarium_maintank_waterlevel" %} input_number.aquarium_maintank_waterlevel
          {% elif trigger.entity_id == "sensor.aquarium_filtertank_waterlevel" %} input_number.aquarium_filtertank_waterlevel
          {% elif trigger.entity_id == "sensor.aquarium_pumptank_waterlevel" %} input_number.aquarium_pumptank_waterlevel
          {% else %} input_number.dummy_number
          {% endif %}
      data:
        value: >
          {% if trigger.entity_id == "sensor.aquarium_pumptank_waterlevel" %}
            {% if trigger.from_state.state == '1' and trigger.to_state.state == '2' %} 2
            {% elif trigger.from_state.state == '2' and trigger.to_state.state == '1' %} 1
            {% elif trigger.from_state.state == '0' and trigger.to_state.state == '1' %} 1
            {% elif trigger.from_state.state == '1' and trigger.to_state.state == '0' %} 0
            {% else %} -1
            {% endif %}
          {% else %}
            {% if trigger.from_state.state == '0' and trigger.to_state.state == '2' %} 2
            {% elif trigger.from_state.state == '2' and trigger.to_state.state == '0' %} 0
            {% else %} -1
            {% endif %}
          {% endif %}
  mode: parallel
  max: 30

到這裡,所有水位都可以清楚地用input_number表示。

要做成動態圖片,不可能根據水位的組合用單一圖片來做替換,因為這樣需要5*3*3*3,總共135張圖片與135條if判斷式。而是改用圖層的概念與CSS的multiply mix-blend-mode來做。

這個動態圖片是由三個層別構成,總共六張圖片。

層別圖片數圖片
1cover.png
4主缸 : main_0.png, main_1.png, main_2.png 依水位擇一
濾材 : filter_0.png, filter_1.png, filter_2.png 依水位擇一
馬達 : pump_0.png, pump_0p5.png, pump_1.png, pump_1p5.png, pump_2.png 依水位擇一
補水桶 : bucket_0.png, bucket_1.png, bucket_2.png 依水位擇一
1base.png

依上中下排序圖片並在css mix-blend-mode中選擇multiply,以下是示意圖

以下是home assistant lovelace的程式碼,可以完整呈現出這種效果。注意程式的順序要由下方圖層依序往上放。

type: picture-elements
image: /local/aquarium/base.png
elements:
  - type: custom:config-template-card
    entities: input_number.aquarium_bucket_waterlevel
    card:
      type: picture
      image: >-
        ${parseInt(states['input_number.aquarium_bucket_waterlevel'].state, 10)
        === 0 ?
            '/local/aquarium/bucket_0.png' : 
            (parseInt(states['input_number.aquarium_bucket_waterlevel'].state, 10) === 1 ? 
            '/local/aquarium/bucket_1.png' : '/local/aquarium/bucket_2.png')}
    style:
      mix-blend-mode: multiply
      top: 50%
      left: 50%
      width: 100%
  - type: custom:config-template-card
    entities: input_number.aquarium_maintank_waterlevel
    card:
      type: picture
      image: >-
        ${parseFloat(states['input_number.aquarium_maintank_waterlevel'].state,
        10) === 0 ?
            '/local/aquarium/main_0.png' : 
            (parseFloat(states['input_number.aquarium_maintank_waterlevel'].state, 10) === 1 ? 
            '/local/aquarium/main_1.png' : '/local/aquarium/main_2.png')}
    style:
      mix-blend-mode: multiply
      top: 50%
      left: 50%
      width: 100%
  - type: custom:config-template-card
    entities: input_number.aquarium_filtertank_waterlevel
    card:
      type: picture
      image: >-
        ${parseFloat(states['input_number.aquarium_filtertank_waterlevel'].state,
        10) === 0 ?
            '/local/aquarium/filter_0.png' : 
            (parseFloat(states['input_number.aquarium_filtertank_waterlevel'].state, 10) === 1 ? 
            '/local/aquarium/filter_1.png' : '/local/aquarium/filter_2.png')}
    style:
      mix-blend-mode: multiply
      top: 50%
      left: 50%
      width: 100%
  - type: custom:config-template-card
    entities: input_number.aquarium_pumptank_waterlevel
    card:
      type: picture
      image: >-
        ${parseFloat(states['input_number.aquarium_pumptank_waterlevel'].state)
        === 0 ?
            '/local/aquarium/pump_0.png' : 
            (parseFloat(states['input_number.aquarium_pumptank_waterlevel'].state) === 0.5 ? 
            '/local/aquarium/pump_0p5.png' :
            (parseFloat(states['input_number.aquarium_pumptank_waterlevel'].state) === 1 ? 
            '/local/aquarium/pump_1.png' : 
            (parseFloat(states['input_number.aquarium_pumptank_waterlevel'].state) === 1.5 ? 
            '/local/aquarium/pump_1p5.png' : '/local/aquarium/pump_2.png')))}
    style:
      mix-blend-mode: multiply
      top: 50%
      left: 50%
      width: 100%
  - type: image
    image: /local/aquarium/cover.png
    style:
      top: 50%
      left: 50%
      width: 100%

回頭跟另一種custom:bar-card做出來的表示法比較,這種方式較為直覺,且水位若不在正常位上,bar-card的顏色會反紅表示。

lovelace程式碼為

type: custom:vertical-stack-in-card
horizontal: true
card_mod:
  style: |
    ha-card {             
      border-radius: 0;
    }
cards:
  - type: custom:bar-card
    name: 主缸
    entity: input_number.aquarium_maintank_waterlevel
    positions:
      icon: 'off'
      indicator: 'off'
      name: outside
    direction: up
    height: 75px
    width: 50%
    max: 2
    min: -1
    color: Pink
    severity:
      - color: lightblue
        from: -0.1
        to: 0.1
    card_mod:
      style: |
        bar-card-backgroundbar {
                  background: rgba(255,255,255,0.4);
                }
  - type: custom:bar-card
    name: 濾材
    entity: input_number.aquarium_filtertank_waterlevel
    positions:
      icon: 'off'
      indicator: 'off'
      name: outside
    direction: up
    height: 75px
    width: 50%
    max: 2
    min: -1
    color: Pink
    severity:
      - color: lightblue
        from: 1.9
        to: 2.1
    card_mod:
      style: |
        bar-card-backgroundbar {
                  background: rgba(255,255,255,0.4);
                }
  - type: custom:bar-card
    name: 馬達
    entity: input_number.aquarium_pumptank_waterlevel
    positions:
      icon: 'off'
      indicator: 'off'
      name: outside
    direction: up
    height: 75px
    width: 50%
    max: 2
    min: -1
    color: Pink
    severity:
      - color: lightblue
        from: 0.9
        to: 1.1
    card_mod:
      style: |
        bar-card-backgroundbar {
                  background: rgba(255,255,255,0.4);
                }
  - type: custom:bar-card
    name: 補水
    entity: input_number.aquarium_bucket_waterlevel
    positions:
      icon: 'off'
      indicator: 'off'
      name: outside
    direction: up
    height: 75px
    width: 50%
    max: 2
    min: -1
    color: Pink
    severity:
      - color: lightblue
        from: 1.9
        to: 2.1
    card_mod:
      style: |
        bar-card-backgroundbar {
                  background: rgba(255,255,255,0.4);
                }

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *

返回頂端