在Home Assistant中做Ubereats的外送進度通知與廣播

沖繩龍蝦

平常沒在拍食物,要用的時候一張都找不到,來個沖繩龍蝦墊墊檔。

通常我下完Ubereats的訂單後,手機可能會丟到一邊去做自己的事,所以外送進度到哪了不知道,外送員到門口了也不知道,等到再想起時,餐點可能已經擺在門外好一陣子了。

所以,提醒我去拿餐點這種事當然交給Home Assistant去處理,讓它把Ubereats外送進度抓下來,視需求做廣播提醒(我只有做外送員抵達時才會廣播提醒)。

整體的概念是利用Cookie裡的sid去抓對應的訂單資訊,所以登入網頁版Ubereats後,按下F12打開「開發人員工具」,下圖是Microsoft Edge瀏覽器的介面,然後

  1. 從左邊工具列中找到應用程式。
  2. 找到儲存空間。
  3. 找到Cookie後點一下展開,點選Ubereats的網址。
  4. 在上方搜尋框中輸入”QA”。
  5. 將「值」複製下來備用,Domain是”.ubereats.com”或是”.uber.com”都一樣。
Uber cookies

這個「值」屬於帳號而非訂單,所以不會因為有新的訂單就有不一樣的「值」,但偶爾還是有遇到「值」有改變,原因沒有深究,可能跟我偶爾會清掉Cookie有關。

回到Home Assistant,流程簡介如下

  1. 名為ubereats_orders_count的rest sensor負責抓指定sid下的訂單數量。
  2. 名為ubereats_has_new_order的binary_sensor負責判斷由1得到的訂單數量,若不為0則為True。
  3. 名為Information:Update_UberEats的automation在ubereats_has_new_order為True時,會以高頻率更新(20秒一次)sensor.ubereats_orders_json。
  4. 名為ubereats_orders_json的rest sensor負責把訂單內容(json)抓下來。
  5. 名為ubereats_order的template sensor負責拆解ubereats_orders_json,把需要的資訊取出當成屬性(attribute)。
  6. 名為ubereats_subtitle的template sensor負責拆解ubereats_orders_json,把外送的進度取出來。
  7. 名為Information:Notify_When_Ubereat_Arrived的automation在sensor.ubereats_subtitle變成”Arriving now”時做廣播。

以下是上述各步驟的程式碼及說明。

步驟1(sensor),Cookie填入前面得到的「值」,payload則視所在地區更改,在台灣的話輸入'{“timezone”:”Asia/Taipei”}’即可。

- platform: rest
  name: UberEats_Orders_Count
  resource: https://www.ubereats.com/api/getActiveOrdersV1
  method: POST
  headers:
    Content-Type: application/json
    X-CSRF-Token: x
    Cookie: "sid=QA.CAESEGCEW2J6wUrTrqJ-iKZeNtoYksPjrQYiATEqJDhiZGIyZjAxLTFkOTEtNGU4Zi1iOTAyLWEzODAwMxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
  payload: '{"timezone":"Asia/Taipei"}'
  force_update: true
  value_template: >
    {{ value_json.data.orders | count | int }}
  unit_of_measurement: "orders"
  scan_interval: 60

步驟2(binary_sensor),將sensor.ubereats_orders_count取integer並寫成變數orders_count,當orders_count不為unknown且大於0時則為True,這一步沒有需要修改的部分。

- platform: template
  sensors:
    ubereats_has_new_order:
      value_template: >
        {% set orders_count = states("sensor.ubereats_orders_count") | int %}
        {{ orders_count != 'unknown' and orders_count > 0 }}

步驟3(automation),condition是需要加上去的,否則Home Assistant會每20秒去更新一次sensor.ubereats_orders_json,但沒有訂單的情況下沒有這個必要。

- alias: Information:Update_UberEats
  trigger:
    - platform: time_pattern
      seconds: "/20"
  condition:
    - condition: state
      entity_id: binary_sensor.ubereats_has_new_order
      state: "on"
  action:
    - service: homeassistant.update_entity
      entity_id: sensor.ubereats_orders_json

步驟4(sensor),與步驟1一樣,更改Cookie與payload。scan_interval設為86400秒是因為沒訂單時這個sensor沒有必要自行以預設的頻率(60秒)更新,而是有訂單的話再從步驟3的automation透過homeassistant.update_entity這個服務來更新即可。

- platform: rest
  name: UberEats_Orders_JSON
  json_attributes:
    - uuid
    - feedCards
    - activeOrderOverview
    - contacts
    - backgroundFeedCards
  json_attributes_path: "$.data.orders[0]"
  resource: https://www.ubereats.com/api/getActiveOrdersV1
  method: POST
  headers:
    Content-Type: application/json
    X-CSRF-Token: x
    Cookie: "sid=QA.CAESEGCEW2J6wUrTrqJ-iKZeNtoYksPjrQYiATEqJDhiZGIyZjAxLTFkOTEtNGU4Zi1iOTAyLWEzODAwMxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
  payload: '{"timezone":"Asia/Taipei"}'
  force_update: true
  value_template: "{{ value_json.data.orders[0].feedCards[0].status.currentProgress }}"
  scan_interval: 86400

步驟5(sensor),從步驟4得到的json中取出需要用到的資訊,例如預計抵達時間、餐廳名稱、外送員名稱與外送員位置(經緯度)等等,但這些屬於過渡資訊,不需要獨立為實體(entity),所以存成sensor.ubereats_order實體下的屬性(attribute)就可以了。

- platform: template
  sensors:
    ubereats_order:
      value_template: >-
        {{ states.sensor.ubereats_orders_json.state }}
      attribute_templates:
        eta: >-
          {{ state_attr("sensor.ubereats_orders_json", "feedCards")[0].status.title  if not is_state("sensor.ubereats_orders_json", "unknown") else "N/A" }}
        title_summary: >-
          {{ state_attr("sensor.ubereats_orders_json", "feedCards")[0].status.titleSummary.summary.text  if not is_state("sensor.ubereats_orders_json", "unknown") else "N/A" }}
        subtitle_summary: >-
          {{ state_attr("sensor.ubereats_orders_json", "feedCards")[0].status.subtitleSummary.summary.text  if not is_state("sensor.ubereats_orders_json", "unknown") else "N/A" }}
        restaurant_name: >-
          {{ state_attr("sensor.ubereats_orders_json", "activeOrderOverview").title if not is_state("sensor.ubereats_orders_json", "unknown") else "N/A" }}
        courier_name: >-
          {{ state_attr("sensor.ubereats_orders_json", "contacts")[0].title if not is_state("sensor.ubereats_orders_json", "unknown") else "N/A" }}
        courier_description: >-
          {{ state_attr("sensor.ubereats_orders_json", "feedCards")[1].courier[0].description if not is_state("sensor.ubereats_orders_json", "unknown") else "N/A" }}
        latitude: >-
          {{ state_attr("sensor.ubereats_orders_json", "backgroundFeedCards")[0].mapEntity[0].latitude if not is_state("sensor.ubereats_orders_json", "unknown") else "N/A" }}
        longitude: >-
          {{ state_attr("sensor.ubereats_orders_json", "backgroundFeedCards")[0].mapEntity[0].longitude if not is_state("sensor.ubereats_orders_json", "unknown") else "N/A" }}

步驟6(sensor),這一條很簡單,應該沒什麼需要解釋的。

- platform: template
  sensors:
    ubereats_subtitle:
      value_template: "{{ states.sensor.ubereats_order.attributes.subtitle_summary }}"

步驟7(automation),action裡面有二個服務(service),第一個是Line通知,第二個是廣播。我在廣播前放了一個condition,當廣播的開關(input_boolean.home_general_broadcast)為開啟時,才會進行廣播,用意是避免在錯誤的時間廣播而打擾到人。

- alias: Information:Notify_When_Ubereat_Arrived
  trigger:
    - platform: template
      value_template: >
        {{ states.sensor.ubereats_subtitle.state == "Arriving now" }}
  action:
    - service: notify.notify_line
      data:
        message: Ubereats外送員已到住家附近。
    - condition: or
      conditions:
        - condition: state
          entity_id: input_boolean.home_general_broadcast
          state: "on"
    - service: tts.google_translate_say
      data:
        entity_id: group.home_broadcast
        message: Ubereats外送員已到住家附近

完成以上就完成了提醒的部分,如果需要在Home Assistant的分頁中放入外送的資訊,Markdown card是一個不錯的選擇。

type: markdown
content: >-
  {% if states.binary_sensor.ubereats_has_new_order.state == "on" %}
  <h2>{{ states.sensor.ubereats_order.attributes.restaurant_name }} </h2>
  <h3>
  {% if states.sensor.ubereats_title.state == "Preparing your order…" %}訂單正在準備中。
  {% elif states.sensor.ubereats_title.state == "Picking up your order…" %}正前往領取訂單。
  {% elif states.sensor.ubereats_title.state == "Heading your way…" %}正前往您所在位置。
  {% elif states.sensor.ubereats_title.state == "Almost here!" %}即將抵達。
  {% endif %}
  </h3>
  <h4>外送員{{ states.sensor.ubereats_order.attributes.courier_name }}預計在 {{ states.sensor.ubereats_remaining.state }} 分鐘後抵達。</h4>
  {% else %}
  <h4>目前無訂單</h4>
  {% endif %}

當中有三個sensor需要另外寫,程式碼如下

- platform: template
  sensors:
    ubereats_title:
      value_template: "{{ states.sensor.ubereats_order.attributes.title_summary }}"

- platform: template
  sensors:
    ubereats_eta:
      value_template: >
        {% if states.sensor.ubereats_order.attributes.eta == 'N/A' %}
          0
        {% else %}
          {% set eta = states.sensor.ubereats_order.attributes.eta %}
          {% set current_date = states.sensor.date.state %}
          {% set hour_offset = 12 if now().hour >= 12 else 0 %}
          {% set uber_date_time = strptime(current_date + ' ' + eta + ':00', '%Y-%m-%d %H:%M:%S') %}
          {% set uber_timestamp = as_timestamp(uber_date_time) + (hour_offset*60*60) %}
          {{ uber_timestamp | timestamp_local }}
        {% endif %}

- platform: template
  sensors:
    ubereats_remaining:
      value_template: >
        {% if states.sensor.ubereats_order.attributes.eta == 'N/A' %}
          0
        {% else %}
          {% set curTime = states.sensor.date_time_iso.state  %}
          {% set orderEta = states.sensor.ubereats_eta.state %}
          {{ ((as_timestamp(orderEta) - as_timestamp(curTime)) / 60) | int }}
        {% endif %}

四種不同進度的結果如下

Ubereats外送狀態

Ubereats在第一筆訂單下訂後10分鐘內再下訂指定店家的商品可以免運費,吃個飯配個飲料理所當然(?

區別是在Ubereats_Order_JSON裡有個json_attributes_path: “$.data.orders[0]”,其中$.data.orders[0]為第一筆訂單,$.data.orders[1]為第二筆訂單,依此類推,可以自己擴充第N筆。

需要這個功能的話複製下面的程式碼進去就可以了,前面文章裡所提的程式碼不用留著。

Automation的部分

- alias: Information:Update_UberEats
  trigger:
    - platform: time_pattern
      seconds: "/20"
  condition:
    - condition: state
      entity_id: binary_sensor.ubereats_has_new_order
      state: "on"
  action:
    - service: homeassistant.update_entity
      entity_id:
        - sensor.ubereats_order_1_json
        - sensor.ubereats_order_2_json
  mode: queued

- alias: Information:Notify_When_Ubereat_Arrived
  trigger:
    - platform: template
      value_template: >
        {{ states.sensor.ubereats_order_1_subtitle.state == "Arriving now" or states.sensor.ubereats_order_2_subtitle.state == "Arriving now" }}
  action:
    - service: notify.notify_line
      data:
        message: Ubereats外送員已到住家附近。
    - condition: or
      conditions:
        - condition: state
          entity_id: input_boolean.home_general_broadcast
          state: "on"
    - service: tts.google_translate_say
      data:
        entity_id: group.home_broadcast
        message: Ubereats外送員已到住家附近
  mode: queued

Binary_sensor的部分(沒有更新)

- platform: template
  sensors:
    ubereats_has_new_order:
      value_template: >
        {% set orders_count = states("sensor.ubereats_orders_count") | int %}
        {{ orders_count != 'unknown' and orders_count > 0 }}

Sensor的部分(另外把外送地址資訊也一起放進來了)

- platform: rest
  name: UberEats_Orders_Count
  resource: https://www.ubereats.com/api/getActiveOrdersV1
  method: POST
  headers:
    Content-Type: application/json
    X-CSRF-Token: x
    Cookie: "sid=QA.CAESEGCEW2J6wUrTrqJ-iKZeNtoYksPjrQYiATEqJDhiZGIyZjAxLTFkOTEtNGU4Zi1iOTAyLWEzODAwMxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
  payload: '{"timezone":"Asia/Taipei"}'
  force_update: true
  value_template: >
    {{ value_json.data.orders | count | int }}
  unit_of_measurement: "orders"
  scan_interval: 60

#Order 1
- platform: rest
  name: UberEats_Order_1_JSON
  json_attributes:
    - uuid
    - feedCards
    - activeOrderOverview
    - contacts
    - backgroundFeedCards
  json_attributes_path: "$.data.orders[0]"
  resource: https://www.ubereats.com/api/getActiveOrdersV1
  method: POST
  headers:
    Content-Type: application/json
    X-CSRF-Token: x
    Cookie: "sid=QA.CAESEGCEW2J6wUrTrqJ-iKZeNtoYksPjrQYiATEqJDhiZGIyZjAxLTFkOTEtNGU4Zi1iOTAyLWEzODAwMxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
  payload: '{"timezone":"Asia/Taipei"}'
  force_update: true
  value_template: "{{ value_json.data.orders[0].feedCards[0].status.currentProgress }}"
  scan_interval: 86400

- platform: template
  sensors:
    ubereats_order_1:
      value_template: >-
        {{ states.sensor.ubereats_order_1_json.state }}
      attribute_templates:
        eta: >-
          {{ state_attr("sensor.ubereats_order_1_json", "feedCards")[0].status.title if not is_state("sensor.ubereats_order_1_json", "unknown") else "N/A" }}
        title_summary: >-
          {{ state_attr("sensor.ubereats_order_1_json", "feedCards")[0].status.titleSummary.summary.text if not is_state("sensor.ubereats_order_1_json", "unknown") else "N/A" }}
        subtitle_summary: >-
          {{ state_attr("sensor.ubereats_order_1_json", "feedCards")[0].status.subtitleSummary.summary.text if not is_state("sensor.ubereats_order_1_json", "unknown") else "N/A" }}
        address: >-
          {{ state_attr("sensor.ubereats_order_1_json", "feedCards")[3].delivery.formattedAddress if not is_state("sensor.ubereats_order_1_json", "unknown") else "N/A" }}
        restaurant_name: >-
          {{ state_attr("sensor.ubereats_order_1_json", "activeOrderOverview").title if not is_state("sensor.ubereats_order_1_json", "unknown") else "N/A" }}
        courier_name: >-
          {{ state_attr("sensor.ubereats_order_1_json", "contacts")[0].title if not is_state("sensor.ubereats_order_1_json", "unknown") else "N/A" }}
        courier_description: >-
          {{ state_attr("sensor.ubereats_order_1_json", "feedCards")[1].courier[0].description if not is_state("sensor.ubereats_order_1_json", "unknown") else "N/A" }}
        latitude: >-
          {{ state_attr("sensor.ubereats_order_1_json", "backgroundFeedCards")[0].mapEntity[0].latitude if not is_state("sensor.ubereats_order_1_json", "unknown") else "N/A" }}
        longitude: >-
          {{ state_attr("sensor.ubereats_order_1_json", "backgroundFeedCards")[0].mapEntity[0].longitude if not is_state("sensor.ubereats_order_1_json", "unknown") else "N/A" }}

- platform: template
  sensors:
    ubereats_order_1_title:
      value_template: "{{ states.sensor.ubereats_order_1.attributes.title_summary }}"

- platform: template
  sensors:
    ubereats_order_1_subtitle:
      value_template: "{{ states.sensor.ubereats_order_1.attributes.subtitle_summary }}"

- platform: template
  sensors:
    ubereats_order_1_eta:
      value_template: >
        {% if states.sensor.ubereats_order_1.attributes.eta == 'N/A' %}
          0
        {% else %}
          {% set eta = states.sensor.ubereats_order_1.attributes.eta %}
          {% set current_date = states.sensor.date.state %}
          {% set hour_offset = 12 if now().hour >= 12 else 0 %}
          {% set uber_date_time = strptime(current_date + ' ' + eta + ':00', '%Y-%m-%d %H:%M:%S') %}
          {% set uber_timestamp = as_timestamp(uber_date_time) + (hour_offset*60*60) %}
          {{ uber_timestamp | timestamp_local }}
        {% endif %}

- platform: template
  sensors:
    ubereats_order_1_remaining:
      value_template: >
        {% if states.sensor.ubereats_order_1.attributes.eta == 'N/A' %}
          0
        {% else %}
          {% set curTime = states.sensor.date_time_iso.state  %}
          {% set orderEta = states.sensor.ubereats_order_1_eta.state %}
          {{ ((as_timestamp(orderEta) - as_timestamp(curTime)) / 60) | int }}
        {% endif %}

#Order 2
- platform: rest
  name: UberEats_Order_2_JSON
  json_attributes:
    - uuid
    - feedCards
    - activeOrderOverview
    - contacts
    - backgroundFeedCards
  json_attributes_path: "$.data.orders[1]"
  resource: https://www.ubereats.com/api/getActiveOrdersV1
  method: POST
  headers:
    Content-Type: application/json
    X-CSRF-Token: x
    Cookie: "sid=QA.CAESEGCEW2J6wUrTrqJ-iKZeNtoYksPjrQYiATEqJDhiZGIyZjAxLTFkOTEtNGU4Zi1iOTAyLWEzODAwMxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
  payload: '{"timezone":"Asia/Taipei"}'
  force_update: true
  value_template: "{{ value_json.data.orders[1].feedCards[0].status.currentProgress }}"
  scan_interval: 86400

- platform: template
  sensors:
    ubereats_order_2:
      value_template: >-
        {{ states.sensor.ubereats_order_2_json.state }}
      attribute_templates:
        eta: >-
          {{ state_attr("sensor.ubereats_order_2_json", "feedCards")[0].status.title if not is_state("sensor.ubereats_order_2_json", "unknown") else "N/A" }}
        title_summary: >-
          {{ state_attr("sensor.ubereats_order_2_json", "feedCards")[0].status.titleSummary.summary.text if not is_state("sensor.ubereats_order_2_json", "unknown") else "N/A" }}
        subtitle_summary: >-
          {{ state_attr("sensor.ubereats_order_2_json", "feedCards")[0].status.subtitleSummary.summary.text if not is_state("sensor.ubereats_order_2_json", "unknown") else "N/A" }}
        address: >-
          {{ state_attr("sensor.ubereats_order_2_json", "feedCards")[3].delivery.formattedAddress if not is_state("sensor.ubereats_order_2_json", "unknown") else "N/A" }}
        restaurant_name: >-
          {{ state_attr("sensor.ubereats_order_2_json", "activeOrderOverview").title if not is_state("sensor.ubereats_order_2_json", "unknown") else "N/A" }}
        courier_name: >-
          {{ state_attr("sensor.ubereats_order_2_json", "contacts")[0].title if not is_state("sensor.ubereats_order_2_json", "unknown") else "N/A" }}
        courier_description: >-
          {{ state_attr("sensor.ubereats_order_2_json", "feedCards")[1].courier[0].description if not is_state("sensor.ubereats_order_2_json", "unknown") else "N/A" }}
        latitude: >-
          {{ state_attr("sensor.ubereats_order_2_json", "backgroundFeedCards")[0].mapEntity[0].latitude if not is_state("sensor.ubereats_order_2_json", "unknown") else "N/A" }}
        longitude: >-
          {{ state_attr("sensor.ubereats_order_2_json", "backgroundFeedCards")[0].mapEntity[0].longitude if not is_state("sensor.ubereats_order_2_json", "unknown") else "N/A" }}

- platform: template
  sensors:
    ubereats_order_2_title:
      value_template: "{{ states.sensor.ubereats_order_2.attributes.title_summary }}"

- platform: template
  sensors:
    ubereats_order_2_subtitle:
      value_template: "{{ states.sensor.ubereats_order_2.attributes.subtitle_summary }}"

- platform: template
  sensors:
    ubereats_order_2_eta:
      value_template: >
        {% if states.sensor.ubereats_order_2.attributes.eta == 'N/A' %}
          0
        {% else %}
          {% set eta = states.sensor.ubereats_order_2.attributes.eta %}
          {% set current_date = states.sensor.date.state %}
          {% set hour_offset = 12 if now().hour >= 12 else 0 %}
          {% set uber_date_time = strptime(current_date + ' ' + eta + ':00', '%Y-%m-%d %H:%M:%S') %}
          {% set uber_timestamp = as_timestamp(uber_date_time) + (hour_offset*60*60) %}
          {{ uber_timestamp | timestamp_local }}
        {% endif %}

- platform: template
  sensors:
    ubereats_order_2_remaining:
      value_template: >
        {% if states.sensor.ubereats_order_2.attributes.eta == 'N/A' %}
          0
        {% else %}
          {% set curTime = states.sensor.date_time_iso.state  %}
          {% set orderEta = states.sensor.ubereats_order_2_eta.state %}
          {{ ((as_timestamp(orderEta) - as_timestamp(curTime)) / 60) | int }}
        {% endif %}

Lovelace Markdown Card的部分

{% if states.sensor.ubereats_order_1_json.state != "unknown" %}
<h2>訂單一 {{ states.sensor.ubereats_order_1.attributes.restaurant_name }} </h2>
<h3>
{% if states.sensor.ubereats_order_1_title.state == "Order received" %}訂單已接受。
{% elif states.sensor.ubereats_order_1_title.state == "Preparing your order…" %}訂單正在準備中。
{% elif states.sensor.ubereats_order_1_title.state == "Picking up your order…" %}正前往領取訂單。
{% elif states.sensor.ubereats_order_1_title.state == "Heading your way…" %}正前往您所在位置。
{% elif states.sensor.ubereats_order_1_title.state == "Almost here!" %}即將抵達。
{% endif %}
</h3>
<h4>外送員{{ states.sensor.ubereats_order_1.attributes.courier_name }}預計再{{ states.sensor.ubereats_order_1_remaining.state }}分鐘後抵達。</h4>
<h4>外送地址為{{ states.sensor.ubereats_order_1.attributes.address }}。</h4>
{% else %}
<h4>訂單一 目前無訂單</h4>
{% endif %}

{% if states.sensor.ubereats_order_2_json.state != "unknown" %}
<h2>訂單二 {{ states.sensor.ubereats_order_2.attributes.restaurant_name }} </h2>
<h3>
{% if states.sensor.ubereats_order_2_title.state == "Order received" %}訂單已接受。
{% elif states.sensor.ubereats_order_2_title.state == "Preparing your order…" %}訂單正在準備中。
{% elif states.sensor.ubereats_order_2_title.state == "Picking up your order…" %}正前往領取訂單。
{% elif states.sensor.ubereats_order_2_title.state == "Heading your way…" %}正前往您所在位置。
{% elif states.sensor.ubereats_order_2_title.state == "Almost here!" %}即將抵達。
{% endif %}
</h3>
<h4>外送員{{ states.sensor.ubereats_order_2.attributes.courier_name }}預計再{{ states.sensor.ubereats_order_2_remaining.state }}分鐘後抵達。</h4>
<h4>外送地址為{{ states.sensor.ubereats_order_2.attributes.address }}。</h4>
{% else %}
<h4>訂單二 目前無訂單</h4>
{% endif %}

如果同時有二筆有效訂單的話,Markdown Card內容會像這樣。

發佈留言

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

返回頂端