【居家辦公】午餐吃什麼的自動化通知

一個會自動通知你午餐要吃什麼的小功能。

自從開始居家工作之後,更注重於時間管理,準時的上下班、午餐時間,固定的儀式感,才不會模糊掉工作與生活的界線

然而除了自己的儀式感之外,也在自家的 HomeAssistant 上簡單的拉了一個自動提醒午餐的功能,減少自己每天花在「想午餐」這件事情上的時間。

發想

每次午餐都不知道吃什麼,都要花時間想,原先只是想說有個簡單的工具可以每次亂數抽一下午餐要吃什麼,上網找了幾個工具,可以使用沒錯,但是好像還是不那麼理想。

後來就想到,不就是簡單的亂數,乾脆直接在 HA 拉一個功能,還可以自動化幾個規則:

  • 固定 12 點鐘觸發
  • 判斷是否為上班日
  • 發送抽午餐通知,順便提醒午餐時間到了

判斷是否為上班日:這是我另一個根據行事曆唯依據的判斷邏輯,簡單的可以用是否為平日判斷即可

設定流程

在 packages 資料夾底下,創建一個 random_lunkch.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
input_select:
my_lunch:
name: 午餐
icon: mdi:food
options:
- 鍋貼

input_text:
new_lunch:
name: 新增午餐文字
icon: mdi:food
today_lunch:
name: 今日午餐文字
lunch_options_temp:
name: 午餐選項暫存

input_button:
new_lunch:
name: 新增午餐
icon: mdi:playlist-plus
remove_lunch:
name: 移除午餐
icon: mdi:playlist-minus
get_lunch:
name: 抽午餐
icon: mdi:bullseye-arrow

然後在 NodeRed 上拉個流程

NodeRed 流程 JSON

1
[{"id":"8248c2f95714d25a","type":"server-state-changed","z":"64a9473eb44f7864","name":"新增午餐","server":"e6c3e1a5.acb55","version":4,"exposeToHomeAssistant":false,"haConfig":[{"property":"name","value":""},{"property":"icon","value":""}],"entityidfilter":"input_button.new_lunch","entityidfiltertype":"exact","outputinitially":false,"state_type":"str","haltifstate":"","halt_if_type":"str","halt_if_compare":"is","outputs":1,"output_only_on_state_change":true,"for":"0","forType":"num","forUnits":"minutes","ignorePrevStateNull":false,"ignorePrevStateUnknown":false,"ignorePrevStateUnavailable":false,"ignoreCurrentStateUnknown":false,"ignoreCurrentStateUnavailable":false,"outputProperties":[{"property":"payload","propertyType":"msg","value":"","valueType":"entityState"},{"property":"data","propertyType":"msg","value":"","valueType":"eventData"},{"property":"topic","propertyType":"msg","value":"","valueType":"triggerId"}],"x":140,"y":180,"wires":[["fbf4d4e147736104"]]},{"id":"123abf5769aeead2","type":"api-call-service","z":"64a9473eb44f7864","name":"新增午餐選項","server":"e6c3e1a5.acb55","version":5,"debugenabled":false,"domain":"input_select","service":"set_options","areaId":[],"deviceId":[],"entityId":["input_select.my_lunch"],"data":"payload","dataType":"jsonata","mergeContext":"","mustacheAltTags":true,"outputProperties":[],"queue":"none","x":500,"y":140,"wires":[["56c93a49b987bb42"]]},{"id":"fbf4d4e147736104","type":"function","z":"64a9473eb44f7864","name":"新午餐選項","func":"const ha = global.get('homeassistant').homeAssistant;\n\n// 取得資料\nlet newOption = ha.states['input_text.new_lunch'].state;\nlet options = ha.states['input_select.my_lunch'].attributes.options;\n\n// 新選項為空 或是 已經在選項內\nif (!newOption || options.includes(newOption)) {\n    return [null, msg];\n}\n\n// 加入到選項中\nlet newOptions = [...options, newOption];\n\n// 設定 msg\nmsg.payload = {\n    \"options\": newOptions\n};\n\nreturn [msg, null];","outputs":2,"noerr":0,"initialize":"","finalize":"","libs":[],"x":310,"y":180,"wires":[["123abf5769aeead2","2e38fb4c7b4e8e61"],["56c93a49b987bb42"]]},{"id":"56c93a49b987bb42","type":"api-call-service","z":"64a9473eb44f7864","name":"清除文字","server":"e6c3e1a5.acb55","version":5,"debugenabled":false,"domain":"input_text","service":"set_value","areaId":[],"deviceId":[],"entityId":["input_text.new_lunch"],"data":"{\"value\":\"\"}","dataType":"jsonata","mergeContext":"","mustacheAltTags":false,"outputProperties":[],"queue":"none","x":680,"y":180,"wires":[[]]},{"id":"21a9c2ae23f0fab0","type":"server-state-changed","z":"64a9473eb44f7864","name":"移除午餐","server":"e6c3e1a5.acb55","version":4,"exposeToHomeAssistant":false,"haConfig":[{"property":"name","value":""},{"property":"icon","value":""}],"entityidfilter":"input_button.remove_lunch","entityidfiltertype":"exact","outputinitially":false,"state_type":"str","haltifstate":"","halt_if_type":"str","halt_if_compare":"is","outputs":1,"output_only_on_state_change":true,"for":"0","forType":"num","forUnits":"minutes","ignorePrevStateNull":false,"ignorePrevStateUnknown":false,"ignorePrevStateUnavailable":false,"ignoreCurrentStateUnknown":false,"ignoreCurrentStateUnavailable":false,"outputProperties":[{"property":"payload","propertyType":"msg","value":"","valueType":"entityState"},{"property":"data","propertyType":"msg","value":"","valueType":"eventData"},{"property":"topic","propertyType":"msg","value":"","valueType":"triggerId"}],"x":140,"y":300,"wires":[["33cac9de44ec1ce8"]]},{"id":"33cac9de44ec1ce8","type":"function","z":"64a9473eb44f7864","name":"新午餐選項","func":"const ha = global.get('homeassistant').homeAssistant;\n\n// helpers functions\nlet arrRemove = (arr, value) => arr.filter((ele) => ele != value);\n\n// 取得資料\nlet currentOption = ha.states['input_select.my_lunch'].state;\nlet options = ha.states['input_select.my_lunch'].attributes.options;\n\n// 複製選項,避免改到原本資料\nlet newOptions = [...options];\n\n// 刪除後要選擇下一個選項\nlet nextOptionIdx = newOptions.indexOf(currentOption) + 1;\nlet nextOption = null;\nif (nextOptionIdx < newOptions.length) {\n    nextOption = newOptions[nextOptionIdx]\n}\n\n// 刪除選項\nnewOptions = arrRemove(newOptions, currentOption);\n\n// 設定 msg\nmsg.payload = {\n    \"options\": newOptions\n};\nmsg.nextOption = nextOption;\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":310,"y":300,"wires":[["38184ea529abe72b","2e38fb4c7b4e8e61"]]},{"id":"38184ea529abe72b","type":"api-call-service","z":"64a9473eb44f7864","name":"新增午餐選項","server":"e6c3e1a5.acb55","version":5,"debugenabled":false,"domain":"input_select","service":"set_options","areaId":[],"deviceId":[],"entityId":["input_select.my_lunch"],"data":"payload","dataType":"jsonata","mergeContext":"","mustacheAltTags":true,"outputProperties":[{"property":"nextOption","propertyType":"msg","value":"nextOption","valueType":"msg"}],"queue":"none","x":500,"y":300,"wires":[["a125dce5821db944"]]},{"id":"a001cfe9e0764ffc","type":"function","z":"64a9473eb44f7864","name":"抽籤","func":"const ha = global.get('homeassistant').homeAssistant;\n\nlet options = [...ha.states['input_select.my_lunch'].attributes.options];\n\n// helpers functions\nlet arrRemove = (arr, value) => arr.filter((ele) => ele != value);\nlet getRandomInt = (min, max) => {\n    min = Math.ceil(min);\n    max = Math.floor(max);\n    return Math.floor(Math.random() * (max - min + 1) + min);\n};\nlet getRandomOption = () => {\n    if (!options.length) {\n        return null;\n    }\n    let randOption = options[getRandomInt(0, options.length -1)];\n    options = arrRemove(options, randOption);\n    return randOption;\n};\n\n// get options\nmsg.payload = [\n    getRandomOption(),\n    getRandomOption()\n]\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":670,"y":440,"wires":[["a35fcec7caecaf8d"]]},{"id":"a35fcec7caecaf8d","type":"template","z":"64a9473eb44f7864","name":"文字","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"今天午餐吃 {{payload.0}},或是 {{payload.1}}","output":"str","x":810,"y":440,"wires":[["97935d0e9c5aad56","5b22215072849558"]]},{"id":"5b22215072849558","type":"api-call-service","z":"64a9473eb44f7864","name":"手機通知","server":"e6c3e1a5.acb55","version":5,"debugenabled":false,"domain":"notify","service":"mobile_app_longiphone","areaId":[],"deviceId":[],"entityId":[],"data":"{\"title\": \"今日午餐\",\"message\":\"{{payload}}\"}","dataType":"json","mergeContext":"","mustacheAltTags":false,"outputProperties":[],"queue":"none","x":960,"y":440,"wires":[[]]},{"id":"97935d0e9c5aad56","type":"api-call-service","z":"64a9473eb44f7864","name":"儲存文字","server":"e6c3e1a5.acb55","version":5,"debugenabled":false,"domain":"input_text","service":"set_value","areaId":[],"deviceId":[],"entityId":["input_text.today_lunch"],"data":"{\"value\":\"{{payload}}\"}","dataType":"json","mergeContext":"","mustacheAltTags":false,"outputProperties":[],"queue":"none","x":960,"y":500,"wires":[[]]},{"id":"df86347c9a1e2308","type":"inject","z":"64a9473eb44f7864","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"00 12 * * *","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":160,"y":440,"wires":[["9c45aebc1d29e211"]]},{"id":"6765a3d308380d08","type":"server-state-changed","z":"64a9473eb44f7864","name":"手動抽午餐","server":"e6c3e1a5.acb55","version":4,"exposeToHomeAssistant":false,"haConfig":[{"property":"name","value":""},{"property":"icon","value":""}],"entityidfilter":"input_button.get_lunch","entityidfiltertype":"exact","outputinitially":false,"state_type":"str","haltifstate":"","halt_if_type":"str","halt_if_compare":"is","outputs":1,"output_only_on_state_change":true,"for":"0","forType":"num","forUnits":"minutes","ignorePrevStateNull":false,"ignorePrevStateUnknown":false,"ignorePrevStateUnavailable":false,"ignoreCurrentStateUnknown":false,"ignoreCurrentStateUnavailable":false,"outputProperties":[{"property":"payload","propertyType":"msg","value":"","valueType":"entityState"},{"property":"data","propertyType":"msg","value":"","valueType":"eventData"},{"property":"topic","propertyType":"msg","value":"","valueType":"triggerId"}],"x":480,"y":520,"wires":[["a001cfe9e0764ffc"]]},{"id":"9c45aebc1d29e211","type":"api-current-state","z":"64a9473eb44f7864","name":"為上班日","server":"e6c3e1a5.acb55","version":3,"outputs":2,"halt_if":"True","halt_if_type":"str","halt_if_compare":"is","entity_id":"sensor.workday","state_type":"str","blockInputOverrides":false,"outputProperties":[{"property":"payload","propertyType":"msg","value":"","valueType":"entityState"},{"property":"data","propertyType":"msg","value":"","valueType":"entity"}],"for":0,"forType":"num","forUnits":"minutes","x":300,"y":440,"wires":[["d4ebf9ce07268bfe"],[]]},{"id":"d4ebf9ce07268bfe","type":"api-current-state","z":"64a9473eb44f7864","name":"在家?","server":"e6c3e1a5.acb55","version":3,"outputs":2,"halt_if":"home","halt_if_type":"str","halt_if_compare":"is","entity_id":"person.long","state_type":"str","blockInputOverrides":false,"outputProperties":[{"property":"data","propertyType":"msg","value":"","valueType":"entity"}],"for":0,"forType":"num","forUnits":"minutes","x":450,"y":440,"wires":[["a001cfe9e0764ffc"],[]]},{"id":"a125dce5821db944","type":"api-call-service","z":"64a9473eb44f7864","name":"設定午餐選項為下一個","server":"e6c3e1a5.acb55","version":5,"debugenabled":false,"domain":"input_select","service":"select_option","areaId":[],"deviceId":[],"entityId":["input_select.my_lunch"],"data":"{\"option\":\"{{nextOption}}\"}","dataType":"json","mergeContext":"","mustacheAltTags":false,"outputProperties":[],"queue":"none","x":720,"y":300,"wires":[[]]},{"id":"200c42378c8756f3","type":"api-call-service","z":"64a9473eb44f7864","name":"重置午餐選項","server":"e6c3e1a5.acb55","version":5,"debugenabled":false,"domain":"input_select","service":"set_options","areaId":[],"deviceId":[],"entityId":["input_select.my_lunch"],"data":"payload","dataType":"jsonata","mergeContext":"","mustacheAltTags":true,"outputProperties":[{"property":"nextOption","propertyType":"msg","value":"nextOption","valueType":"msg"}],"queue":"none","x":640,"y":660,"wires":[[]]},{"id":"6a795a0d9e9b5171","type":"function","z":"64a9473eb44f7864","name":"抓取暫存午餐選項","func":"const ha = global.get('homeassistant').homeAssistant;\n\n\n// 取得資料\nlet options = ha.states['input_text.lunch_options_temp'].state;\n\n// 設定 msg\nmsg.payload = {\n    \"options\": options.split(','),\n};\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":430,"y":660,"wires":[["200c42378c8756f3"]]},{"id":"2e38fb4c7b4e8e61","type":"function","z":"64a9473eb44f7864","name":"暫存選項","func":"msg.payload = {\n    \"value\": msg.payload.options.join(',')\n}\nreturn msg","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":480,"y":240,"wires":[["92da02e3390b285e"]]},{"id":"92da02e3390b285e","type":"api-call-service","z":"64a9473eb44f7864","name":"暫存選項","server":"e6c3e1a5.acb55","version":5,"debugenabled":false,"domain":"input_text","service":"set_value","areaId":[],"deviceId":[],"entityId":["input_text.lunch_options_temp"],"data":"payload","dataType":"jsonata","mergeContext":"","mustacheAltTags":false,"outputProperties":[],"queue":"none","x":640,"y":240,"wires":[[]]},{"id":"fb8fdd1a7bbdeb3d","type":"link in","z":"64a9473eb44f7864","name":"Home Assistant Restarted","links":["3754cbf709193071"],"x":190,"y":660,"wires":[["6a795a0d9e9b5171"]],"l":true},{"id":"e6c3e1a5.acb55","type":"server","name":"Home Assistant","version":4,"addon":true,"rejectUnauthorizedCerts":true,"ha_boolean":"y|yes|true|on|home|open","connectionDelay":true,"cacheJson":true,"heartbeat":false,"heartbeatInterval":30,"areaSelector":"friendlyName","deviceSelector":"friendlyName","entitySelector":"friendlyName","statusSeparator":"at: ","statusYear":"hidden","statusMonth":"short","statusDay":"numeric","statusHourCycle":"h23","statusTimeFormat":"h:m"}]

由於 HA 再重新啟動後,input_select 的選項會被重置,所以圖片中最下面那個是需要搭配 Starting a flow after a Home Assistant restart 把暫存的選項抓回來。

之後再把那些元件放在 HA 的總覽上,就可以在這邊新增/刪除選項,或是手動觸發。

結論

之後每天就會有固定收到通知,提醒你午餐時間,並幫你想好午餐可以吃什麼。

這是第一次完整在 HA 上弄不是串接硬體裝置的功能,還蠻有趣的,透過這個功能每天省下的時間,可以拿去做更多想做的事情~

參考