[source/keys:feat] Nest Thermostat bindings

This commit is contained in:
Maddison Hellstrom 2020-11-17 05:36:38 -08:00
parent bd726a168a
commit 6c5e38f480
3 changed files with 188 additions and 25 deletions

View File

@ -704,4 +704,90 @@ actions.wp.toggleSimple = () => {
actions.openLink(u.href)()
}
// Nest Thermostat Controller
// --------------------------
actions.nt = {}
actions.nt.adjustTemp = (dir) => () =>
document.querySelector(
`button[data-test='thermozilla-controller-controls-${dir > 0 ? "in" : "de"}crement-button']`,
).click()
actions.nt.setMode = (mode) => async () => {
const selectMode = async (popover) => {
const query = () => !popover.isConnected
const q = query()
if (q) return q
popover.querySelector(`button[data-test='thermozilla-mode-switcher-${mode}-button']`).click()
return util.until(query)
}
const openPopover = async () => {
const query = () => document.querySelector("div[data-test='thermozilla-mode-popover']")
const q = query()
if (q) return q
document.querySelector("button[data-test='thermozilla-mode-button']").click()
return util.until(query)
}
const popover = await openPopover()
return selectMode(popover)
}
actions.nt.setFan = (desiredState) => async () => {
const startStopFan = async (startStop, popover) => {
const query = () => !popover.isConnected
const q = query()
if (q) return q
popover.querySelector(`div[data-test='thermozilla-fan-timer-${startStop}-button']`).click()
return util.until(query)
}
const selectFanTime = async (popover, listbox) => {
const query = () => !listbox.isConnected
const q = query()
if (q) return q
Hints.dispatchMouseClick(listbox.querySelector("div[role='option']:last-child"))
return util.until(query)
}
const openFanListbox = async (popover) => {
const query = () => popover.querySelector("div[role='listbox']")
const q = query()
if (q) return q
Hints.dispatchMouseClick(popover.querySelector("div[role='combobox']"))
return util.until(query)
}
const openPopover = async () => {
const query = () => document.querySelector("div[data-test='thermozilla-fan-timer-popover']")
const q = query()
if (q) return q
document.querySelector("button[data-test='thermozilla-fan-button']").click()
return util.until(query)
}
const fanRunning = () => document.querySelector("div[data-test='thermozilla-aag-fan-listcell-title']")
const startFan = async () => {
const popover = await openPopover()
const listbox = await openFanListbox(popover)
await selectFanTime(popover, listbox)
return startStopFan("start", popover)
}
const stopFan = async () => {
const popover = await openPopover()
await startStopFan("stop", popover)
await util.until(() => !fanRunning())
}
if (fanRunning()) {
await stopFan()
}
if (desiredState === 1) {
await startFan()
}
}
module.exports = actions

53
keys.js
View File

@ -758,6 +758,59 @@ const maps = {
callback: actions.createHint("a[href^='/packages/'][href$='/']"),
},
],
"home.nest.com": [
{
path: "/thermostat/DEVICE_.*",
leader: "",
alias: ["+", "="],
description: "Increment temperature",
callback: actions.nt.adjustTemp(1),
},
{
path: "/thermostat/DEVICE_.*",
leader: "",
alias: ["-", "_"],
description: "Decrement temperature",
callback: actions.nt.adjustTemp(-1),
},
{
path: "/thermostat/DEVICE_.*",
alias: "h",
description: "Switch mode to Heat",
callback: actions.nt.setMode("heat"),
},
{
path: "/thermostat/DEVICE_.*",
alias: "c",
description: "Switch mode to Cool",
callback: actions.nt.setMode("cool"),
},
{
path: "/thermostat/DEVICE_.*",
alias: "r",
description: "Switch mode to Heat/Cool",
callback: actions.nt.setMode("range"),
},
{
path: "/thermostat/DEVICE_.*",
alias: "o",
description: "Switch mode to Off",
callback: actions.nt.setMode("off"),
},
{
path: "/thermostat/DEVICE_.*",
alias: "f",
description: "Switch fan On",
callback: actions.nt.setFan(1),
},
{
path: "/thermostat/DEVICE_.*",
alias: "F",
description: "Switch fan Off",
callback: actions.nt.setFan(0),
},
],
}
// Aliases

40
util.js
View File

@ -9,7 +9,8 @@ util.getCurrentLocation = (prop = "href") => {
return window.location[prop]
}
util.escape = (str) => String(str).replace(/[&<>"'`=/]/g, (s) => ({
util.escape = (str) =>
String(str).replace(/[&<>"'`=/]/g, (s) => ({
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
@ -18,7 +19,28 @@ util.escape = (str) => String(str).replace(/[&<>"'`=/]/g, (s) => ({
"/": "&#x2F;",
"`": "&#x60;",
"=": "&#x3D;",
}[s]))
}[s]))
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#Escaping
util.escapeRegExp = (str) =>
str.replace(/[.*+\-?^${}()|[\]\\]/g, "\\$&")
util.until = (check, test = (a) => a, maxAttempts = 50, interval = 50) =>
new Promise((resolve, reject) => {
const f = (attempts = 0) => {
const res = check()
if (!test(res)) {
if (attempts > maxAttempts) {
reject(new Error("until: timeout"))
} else {
setTimeout(() => f(attempts + 1), interval)
}
return
}
resolve(res)
}
f()
})
util.createSuggestionItem = (html, props = {}) => {
const li = document.createElement("li")
@ -98,24 +120,26 @@ util.processMaps = (maps, aliases, siteleader) => {
leader = (domain === "global") ? "" : siteleader,
category = categories.misc,
description = "",
} = mapObj
path = "(/.*)?",
} = mapObj;
(Array.isArray(alias) ? alias : [alias]).forEach((a) => {
const opts = {}
const key = `${leader}${alias}`
const key = `${leader}${a}`
// Determine if it's a site-specific mapping
if (domain !== "global") {
const d = domain.replace(".", "\\.")
opts.domain = new RegExp(`^http(s)?://(([a-zA-Z0-9-_]+\\.)*)(${d})(/.*)?`)
const d = util.escapeRegExp(domain)
opts.domain = new RegExp(`^http(s)?://(([a-zA-Z0-9-_]+\\.)*)(${d})${path}`)
}
const fullDescription = `#${category} ${description}`
if (mapObj.map !== undefined) {
map(alias, mapObj.map)
map(a, mapObj.map)
} else {
mapkey(key, fullDescription, callback, opts)
}
})
})))
}