簡單地講,這是把魚缸的水位用一組示意圖來表示,圖中藍色的水量會隨著水位的值而變動。例如,中間這個是一個背濾槽示意圖,總共隔了三格,由左到右分別為入水格、濾材格與馬達格,最右邊馬達格的水位,現在落在上下限之間。
這是一個不做覺得可惜但做了又覺得沒用的東西,主因是沒這麼直覺好讀。
先從頭講起,我的魚缸總共有設置了四個水位的監測
- 主缸:一顆浮球開關,正常情況為低水位,若在高水位會有背濾白棉堵塞且嚴重時水會溢出缸外的風險。
- 背濾的濾材格:一顆浮球開關,正常情況為高水位,若在低水位時水位無法完全覆蓋陶瓷環。
- 背濾的馬達格:二顆浮球開關組成低中高水位(*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來做。
這個動態圖片是由三個層別構成,總共六張圖片。
層別 | 圖片數 | 圖片 |
上 | 1 | cover.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 依水位擇一 |
下 | 1 | base.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);
}