Articles Categories Tags About
【居家辦公】午餐吃什麼的自動化通知
Smart Home · 2 min read

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

透過 Home Assistant 打造自動午餐提醒功能,減少居家辦公時每天花在「想午餐」上的時間。

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

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

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

發想

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

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

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

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

設定流程

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

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 上拉個流程

[
  {
    "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 上弄不是串接硬體裝置的功能,還蠻有趣的,透過這個功能每天省下的時間,可以拿去做更多想做的事情~

參考



Logan

Senior software engineer, passionate about coding and smart home 🏠

Licensed under CC BY-NC 4.0


Share:


Related Articles


Type to search articles