diff --git a/actions.js b/actions.js index d82898b..afad164 100644 --- a/actions.js +++ b/actions.js @@ -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 diff --git a/keys.js b/keys.js index 73fe680..08fb244 100644 --- a/keys.js +++ b/keys.js @@ -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 diff --git a/util.js b/util.js index 76d772a..c311616 100644 --- a/util.js +++ b/util.js @@ -9,16 +9,38 @@ util.getCurrentLocation = (prop = "href") => { return window.location[prop] } -util.escape = (str) => String(str).replace(/[&<>"'`=/]/g, (s) => ({ - "&": "&", - "<": "<", - ">": ">", - "\"": """, - "'": "'", - "/": "/", - "`": "`", - "=": "=", -}[s])) +util.escape = (str) => + String(str).replace(/[&<>"'`=/]/g, (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 - const opts = {} + path = "(/.*)?", + } = mapObj; + (Array.isArray(alias) ? alias : [alias]).forEach((a) => { + const opts = {} + const key = `${leader}${a}` - const key = `${leader}${alias}` + // Determine if it's a site-specific mapping + if (domain !== "global") { + const d = util.escapeRegExp(domain) + opts.domain = new RegExp(`^http(s)?://(([a-zA-Z0-9-_]+\\.)*)(${d})${path}`) + } - // 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 fullDescription = `#${category} ${description}` - const fullDescription = `#${category} ${description}` - - if (mapObj.map !== undefined) { - map(alias, mapObj.map) - } else { - mapkey(key, fullDescription, callback, opts) - } + if (mapObj.map !== undefined) { + map(a, mapObj.map) + } else { + mapkey(key, fullDescription, callback, opts) + } + }) }))) }