1055 lines
32 KiB
JavaScript
1055 lines
32 KiB
JavaScript
// node_modules/home-assistant-js-websocket/dist/errors.js
|
|
var ERR_CANNOT_CONNECT = 1;
|
|
var ERR_INVALID_AUTH = 2;
|
|
var ERR_CONNECTION_LOST = 3;
|
|
var ERR_HASS_HOST_REQUIRED = 4;
|
|
var ERR_INVALID_HTTPS_TO_HTTP = 5;
|
|
var ERR_INVALID_AUTH_CALLBACK = 6;
|
|
|
|
// node_modules/home-assistant-js-websocket/dist/messages.js
|
|
function auth(accessToken) {
|
|
return {
|
|
type: "auth",
|
|
access_token: accessToken
|
|
};
|
|
}
|
|
function supportedFeatures() {
|
|
return {
|
|
type: "supported_features",
|
|
id: 1,
|
|
// Always the first message after auth
|
|
features: { coalesce_messages: 1 }
|
|
};
|
|
}
|
|
function states() {
|
|
return {
|
|
type: "get_states"
|
|
};
|
|
}
|
|
function config() {
|
|
return {
|
|
type: "get_config"
|
|
};
|
|
}
|
|
function services() {
|
|
return {
|
|
type: "get_services"
|
|
};
|
|
}
|
|
function user() {
|
|
return {
|
|
type: "auth/current_user"
|
|
};
|
|
}
|
|
function callService(domain, service, serviceData, target, returnResponse) {
|
|
const message = {
|
|
type: "call_service",
|
|
domain,
|
|
service,
|
|
target,
|
|
return_response: returnResponse
|
|
};
|
|
if (serviceData) {
|
|
message.service_data = serviceData;
|
|
}
|
|
return message;
|
|
}
|
|
function subscribeEvents(eventType) {
|
|
const message = {
|
|
type: "subscribe_events"
|
|
};
|
|
if (eventType) {
|
|
message.event_type = eventType;
|
|
}
|
|
return message;
|
|
}
|
|
function unsubscribeEvents(subscription) {
|
|
return {
|
|
type: "unsubscribe_events",
|
|
subscription
|
|
};
|
|
}
|
|
function ping() {
|
|
return {
|
|
type: "ping"
|
|
};
|
|
}
|
|
function error(code, message) {
|
|
return {
|
|
type: "result",
|
|
success: false,
|
|
error: {
|
|
code,
|
|
message
|
|
}
|
|
};
|
|
}
|
|
|
|
// node_modules/home-assistant-js-websocket/dist/util.js
|
|
function parseQuery(queryString) {
|
|
const query = {};
|
|
const items = queryString.split("&");
|
|
for (let i = 0; i < items.length; i++) {
|
|
const item = items[i].split("=");
|
|
const key = decodeURIComponent(item[0]);
|
|
const value = item.length > 1 ? decodeURIComponent(item[1]) : void 0;
|
|
query[key] = value;
|
|
}
|
|
return query;
|
|
}
|
|
var debounce = (func, wait, immediate = false) => {
|
|
let timeout;
|
|
return function(...args) {
|
|
const context = this;
|
|
const later = () => {
|
|
timeout = void 0;
|
|
if (!immediate) {
|
|
func.apply(context, args);
|
|
}
|
|
};
|
|
const callNow = immediate && !timeout;
|
|
clearTimeout(timeout);
|
|
timeout = setTimeout(later, wait);
|
|
if (callNow) {
|
|
func.apply(context, args);
|
|
}
|
|
};
|
|
};
|
|
var atLeastHaVersion = (version, major, minor, patch) => {
|
|
const [haMajor, haMinor, haPatch] = version.split(".", 3);
|
|
return Number(haMajor) > major || Number(haMajor) === major && (patch === void 0 ? Number(haMinor) >= minor : Number(haMinor) > minor) || patch !== void 0 && Number(haMajor) === major && Number(haMinor) === minor && Number(haPatch) >= patch;
|
|
};
|
|
|
|
// node_modules/home-assistant-js-websocket/dist/socket.js
|
|
var DEBUG = false;
|
|
var MSG_TYPE_AUTH_REQUIRED = "auth_required";
|
|
var MSG_TYPE_AUTH_INVALID = "auth_invalid";
|
|
var MSG_TYPE_AUTH_OK = "auth_ok";
|
|
function createSocket(options) {
|
|
if (!options.auth) {
|
|
throw ERR_HASS_HOST_REQUIRED;
|
|
}
|
|
const auth2 = options.auth;
|
|
let authRefreshTask = auth2.expired ? auth2.refreshAccessToken().then(() => {
|
|
authRefreshTask = void 0;
|
|
}, () => {
|
|
authRefreshTask = void 0;
|
|
}) : void 0;
|
|
const url = auth2.wsUrl;
|
|
if (DEBUG) {
|
|
console.log("[Auth phase] Initializing", url);
|
|
}
|
|
function connect(triesLeft, promResolve, promReject) {
|
|
if (DEBUG) {
|
|
console.log("[Auth Phase] New connection", url);
|
|
}
|
|
const socket = new WebSocket(url);
|
|
let invalidAuth = false;
|
|
const closeMessage = () => {
|
|
socket.removeEventListener("close", closeMessage);
|
|
if (invalidAuth) {
|
|
promReject(ERR_INVALID_AUTH);
|
|
return;
|
|
}
|
|
if (triesLeft === 0) {
|
|
promReject(ERR_CANNOT_CONNECT);
|
|
return;
|
|
}
|
|
const newTries = triesLeft === -1 ? -1 : triesLeft - 1;
|
|
setTimeout(() => connect(newTries, promResolve, promReject), 1e3);
|
|
};
|
|
const handleOpen = async (event) => {
|
|
try {
|
|
if (auth2.expired) {
|
|
await (authRefreshTask ? authRefreshTask : auth2.refreshAccessToken());
|
|
}
|
|
socket.send(JSON.stringify(auth(auth2.accessToken)));
|
|
} catch (err) {
|
|
invalidAuth = err === ERR_INVALID_AUTH;
|
|
socket.close();
|
|
}
|
|
};
|
|
const handleMessage = async (event) => {
|
|
const message = JSON.parse(event.data);
|
|
if (DEBUG) {
|
|
console.log("[Auth phase] Received", message);
|
|
}
|
|
switch (message.type) {
|
|
case MSG_TYPE_AUTH_INVALID:
|
|
invalidAuth = true;
|
|
socket.close();
|
|
break;
|
|
case MSG_TYPE_AUTH_OK:
|
|
socket.removeEventListener("open", handleOpen);
|
|
socket.removeEventListener("message", handleMessage);
|
|
socket.removeEventListener("close", closeMessage);
|
|
socket.removeEventListener("error", closeMessage);
|
|
socket.haVersion = message.ha_version;
|
|
if (atLeastHaVersion(socket.haVersion, 2022, 9)) {
|
|
socket.send(JSON.stringify(supportedFeatures()));
|
|
}
|
|
promResolve(socket);
|
|
break;
|
|
default:
|
|
if (DEBUG) {
|
|
if (message.type !== MSG_TYPE_AUTH_REQUIRED) {
|
|
console.warn("[Auth phase] Unhandled message", message);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
socket.addEventListener("open", handleOpen);
|
|
socket.addEventListener("message", handleMessage);
|
|
socket.addEventListener("close", closeMessage);
|
|
socket.addEventListener("error", closeMessage);
|
|
}
|
|
return new Promise((resolve, reject) => connect(options.setupRetry, resolve, reject));
|
|
}
|
|
|
|
// node_modules/home-assistant-js-websocket/dist/connection.js
|
|
var DEBUG2 = false;
|
|
var Connection = class {
|
|
constructor(socket, options) {
|
|
this._handleMessage = (event) => {
|
|
let messageGroup = JSON.parse(event.data);
|
|
if (!Array.isArray(messageGroup)) {
|
|
messageGroup = [messageGroup];
|
|
}
|
|
messageGroup.forEach((message) => {
|
|
if (DEBUG2) {
|
|
console.log("Received", message);
|
|
}
|
|
const info = this.commands.get(message.id);
|
|
switch (message.type) {
|
|
case "event":
|
|
if (info) {
|
|
info.callback(message.event);
|
|
} else {
|
|
console.warn(`Received event for unknown subscription ${message.id}. Unsubscribing.`);
|
|
this.sendMessagePromise(unsubscribeEvents(message.id)).catch((err) => {
|
|
if (DEBUG2) {
|
|
console.warn(` Error unsubsribing from unknown subscription ${message.id}`, err);
|
|
}
|
|
});
|
|
}
|
|
break;
|
|
case "result":
|
|
if (info) {
|
|
if (message.success) {
|
|
info.resolve(message.result);
|
|
if (!("subscribe" in info)) {
|
|
this.commands.delete(message.id);
|
|
}
|
|
} else {
|
|
info.reject(message.error);
|
|
this.commands.delete(message.id);
|
|
}
|
|
}
|
|
break;
|
|
case "pong":
|
|
if (info) {
|
|
info.resolve();
|
|
this.commands.delete(message.id);
|
|
} else {
|
|
console.warn(`Received unknown pong response ${message.id}`);
|
|
}
|
|
break;
|
|
default:
|
|
if (DEBUG2) {
|
|
console.warn("Unhandled message", message);
|
|
}
|
|
}
|
|
});
|
|
};
|
|
this._handleClose = async () => {
|
|
const oldCommands = this.commands;
|
|
this.commandId = 1;
|
|
this.oldSubscriptions = this.commands;
|
|
this.commands = /* @__PURE__ */ new Map();
|
|
this.socket = void 0;
|
|
oldCommands.forEach((info) => {
|
|
if (!("subscribe" in info)) {
|
|
info.reject(error(ERR_CONNECTION_LOST, "Connection lost"));
|
|
}
|
|
});
|
|
if (this.closeRequested) {
|
|
return;
|
|
}
|
|
this.fireEvent("disconnected");
|
|
const options2 = Object.assign(Object.assign({}, this.options), { setupRetry: 0 });
|
|
const reconnect = (tries) => {
|
|
setTimeout(async () => {
|
|
if (this.closeRequested) {
|
|
return;
|
|
}
|
|
if (DEBUG2) {
|
|
console.log("Trying to reconnect");
|
|
}
|
|
try {
|
|
const socket2 = await options2.createSocket(options2);
|
|
this._setSocket(socket2);
|
|
} catch (err) {
|
|
if (this._queuedMessages) {
|
|
const queuedMessages = this._queuedMessages;
|
|
this._queuedMessages = void 0;
|
|
for (const msg of queuedMessages) {
|
|
if (msg.reject) {
|
|
msg.reject(ERR_CONNECTION_LOST);
|
|
}
|
|
}
|
|
}
|
|
if (err === ERR_INVALID_AUTH) {
|
|
this.fireEvent("reconnect-error", err);
|
|
} else {
|
|
reconnect(tries + 1);
|
|
}
|
|
}
|
|
}, Math.min(tries, 5) * 1e3);
|
|
};
|
|
if (this.suspendReconnectPromise) {
|
|
await this.suspendReconnectPromise;
|
|
this.suspendReconnectPromise = void 0;
|
|
this._queuedMessages = [];
|
|
}
|
|
reconnect(0);
|
|
};
|
|
this.options = options;
|
|
this.commandId = 2;
|
|
this.commands = /* @__PURE__ */ new Map();
|
|
this.eventListeners = /* @__PURE__ */ new Map();
|
|
this.closeRequested = false;
|
|
this._setSocket(socket);
|
|
}
|
|
get connected() {
|
|
return this.socket !== void 0 && this.socket.readyState == this.socket.OPEN;
|
|
}
|
|
_setSocket(socket) {
|
|
this.socket = socket;
|
|
this.haVersion = socket.haVersion;
|
|
socket.addEventListener("message", this._handleMessage);
|
|
socket.addEventListener("close", this._handleClose);
|
|
const oldSubscriptions = this.oldSubscriptions;
|
|
if (oldSubscriptions) {
|
|
this.oldSubscriptions = void 0;
|
|
oldSubscriptions.forEach((info) => {
|
|
if ("subscribe" in info && info.subscribe) {
|
|
info.subscribe().then((unsub) => {
|
|
info.unsubscribe = unsub;
|
|
info.resolve();
|
|
});
|
|
}
|
|
});
|
|
}
|
|
const queuedMessages = this._queuedMessages;
|
|
if (queuedMessages) {
|
|
this._queuedMessages = void 0;
|
|
for (const queuedMsg of queuedMessages) {
|
|
queuedMsg.resolve();
|
|
}
|
|
}
|
|
this.fireEvent("ready");
|
|
}
|
|
addEventListener(eventType, callback) {
|
|
let listeners = this.eventListeners.get(eventType);
|
|
if (!listeners) {
|
|
listeners = [];
|
|
this.eventListeners.set(eventType, listeners);
|
|
}
|
|
listeners.push(callback);
|
|
}
|
|
removeEventListener(eventType, callback) {
|
|
const listeners = this.eventListeners.get(eventType);
|
|
if (!listeners) {
|
|
return;
|
|
}
|
|
const index = listeners.indexOf(callback);
|
|
if (index !== -1) {
|
|
listeners.splice(index, 1);
|
|
}
|
|
}
|
|
fireEvent(eventType, eventData) {
|
|
(this.eventListeners.get(eventType) || []).forEach((callback) => callback(this, eventData));
|
|
}
|
|
suspendReconnectUntil(suspendPromise) {
|
|
this.suspendReconnectPromise = suspendPromise;
|
|
}
|
|
suspend() {
|
|
if (!this.suspendReconnectPromise) {
|
|
throw new Error("Suspend promise not set");
|
|
}
|
|
if (this.socket) {
|
|
this.socket.close();
|
|
}
|
|
}
|
|
/**
|
|
* Reconnect the websocket connection.
|
|
* @param force discard old socket instead of gracefully closing it.
|
|
*/
|
|
reconnect(force = false) {
|
|
if (!this.socket) {
|
|
return;
|
|
}
|
|
if (!force) {
|
|
this.socket.close();
|
|
return;
|
|
}
|
|
this.socket.removeEventListener("message", this._handleMessage);
|
|
this.socket.removeEventListener("close", this._handleClose);
|
|
this.socket.close();
|
|
this._handleClose();
|
|
}
|
|
close() {
|
|
this.closeRequested = true;
|
|
if (this.socket) {
|
|
this.socket.close();
|
|
}
|
|
}
|
|
/**
|
|
* Subscribe to a specific or all events.
|
|
*
|
|
* @param callback Callback to be called when a new event fires
|
|
* @param eventType
|
|
* @returns promise that resolves to an unsubscribe function
|
|
*/
|
|
async subscribeEvents(callback, eventType) {
|
|
return this.subscribeMessage(callback, subscribeEvents(eventType));
|
|
}
|
|
ping() {
|
|
return this.sendMessagePromise(ping());
|
|
}
|
|
sendMessage(message, commandId) {
|
|
if (!this.connected) {
|
|
throw ERR_CONNECTION_LOST;
|
|
}
|
|
if (DEBUG2) {
|
|
console.log("Sending", message);
|
|
}
|
|
if (this._queuedMessages) {
|
|
if (commandId) {
|
|
throw new Error("Cannot queue with commandId");
|
|
}
|
|
this._queuedMessages.push({ resolve: () => this.sendMessage(message) });
|
|
return;
|
|
}
|
|
if (!commandId) {
|
|
commandId = this._genCmdId();
|
|
}
|
|
message.id = commandId;
|
|
this.socket.send(JSON.stringify(message));
|
|
}
|
|
sendMessagePromise(message) {
|
|
return new Promise((resolve, reject) => {
|
|
if (this._queuedMessages) {
|
|
this._queuedMessages.push({
|
|
reject,
|
|
resolve: async () => {
|
|
try {
|
|
resolve(await this.sendMessagePromise(message));
|
|
} catch (err) {
|
|
reject(err);
|
|
}
|
|
}
|
|
});
|
|
return;
|
|
}
|
|
const commandId = this._genCmdId();
|
|
this.commands.set(commandId, { resolve, reject });
|
|
this.sendMessage(message, commandId);
|
|
});
|
|
}
|
|
/**
|
|
* Call a websocket command that starts a subscription on the backend.
|
|
*
|
|
* @param message the message to start the subscription
|
|
* @param callback the callback to be called when a new item arrives
|
|
* @param [options.resubscribe] re-established a subscription after a reconnect. Defaults to true.
|
|
* @returns promise that resolves to an unsubscribe function
|
|
*/
|
|
async subscribeMessage(callback, subscribeMessage, options) {
|
|
if (this._queuedMessages) {
|
|
await new Promise((resolve, reject) => {
|
|
this._queuedMessages.push({ resolve, reject });
|
|
});
|
|
}
|
|
if (options === null || options === void 0 ? void 0 : options.preCheck) {
|
|
const precheck = await options.preCheck();
|
|
if (!precheck) {
|
|
throw new Error("Pre-check failed");
|
|
}
|
|
}
|
|
let info;
|
|
await new Promise((resolve, reject) => {
|
|
const commandId = this._genCmdId();
|
|
info = {
|
|
resolve,
|
|
reject,
|
|
callback,
|
|
subscribe: (options === null || options === void 0 ? void 0 : options.resubscribe) !== false ? () => this.subscribeMessage(callback, subscribeMessage, options) : void 0,
|
|
unsubscribe: async () => {
|
|
if (this.connected) {
|
|
await this.sendMessagePromise(unsubscribeEvents(commandId));
|
|
}
|
|
this.commands.delete(commandId);
|
|
}
|
|
};
|
|
this.commands.set(commandId, info);
|
|
try {
|
|
this.sendMessage(subscribeMessage, commandId);
|
|
} catch (err) {
|
|
}
|
|
});
|
|
return () => info.unsubscribe();
|
|
}
|
|
_genCmdId() {
|
|
return ++this.commandId;
|
|
}
|
|
};
|
|
|
|
// node_modules/home-assistant-js-websocket/dist/auth.js
|
|
var genClientId = () => `${location.protocol}//${location.host}/`;
|
|
var genExpires = (expires_in) => {
|
|
return expires_in * 1e3 + Date.now();
|
|
};
|
|
function genRedirectUrl() {
|
|
const { protocol, host, pathname, search } = location;
|
|
return `${protocol}//${host}${pathname}${search}`;
|
|
}
|
|
function genAuthorizeUrl(hassUrl, clientId, redirectUrl, state) {
|
|
let authorizeUrl = `${hassUrl}/auth/authorize?response_type=code&redirect_uri=${encodeURIComponent(redirectUrl)}`;
|
|
if (clientId !== null) {
|
|
authorizeUrl += `&client_id=${encodeURIComponent(clientId)}`;
|
|
}
|
|
if (state) {
|
|
authorizeUrl += `&state=${encodeURIComponent(state)}`;
|
|
}
|
|
return authorizeUrl;
|
|
}
|
|
function redirectAuthorize(hassUrl, clientId, redirectUrl, state) {
|
|
redirectUrl += (redirectUrl.includes("?") ? "&" : "?") + "auth_callback=1";
|
|
document.location.href = genAuthorizeUrl(hassUrl, clientId, redirectUrl, state);
|
|
}
|
|
async function tokenRequest(hassUrl, clientId, data) {
|
|
const l = typeof location !== "undefined" && location;
|
|
if (l && l.protocol === "https:") {
|
|
const a = document.createElement("a");
|
|
a.href = hassUrl;
|
|
if (a.protocol === "http:" && a.hostname !== "localhost") {
|
|
throw ERR_INVALID_HTTPS_TO_HTTP;
|
|
}
|
|
}
|
|
const formData = new FormData();
|
|
if (clientId !== null) {
|
|
formData.append("client_id", clientId);
|
|
}
|
|
Object.keys(data).forEach((key) => {
|
|
formData.append(key, data[key]);
|
|
});
|
|
const resp = await fetch(`${hassUrl}/auth/token`, {
|
|
method: "POST",
|
|
credentials: "same-origin",
|
|
body: formData
|
|
});
|
|
if (!resp.ok) {
|
|
throw resp.status === 400 || resp.status === 403 ? ERR_INVALID_AUTH : new Error("Unable to fetch tokens");
|
|
}
|
|
const tokens = await resp.json();
|
|
tokens.hassUrl = hassUrl;
|
|
tokens.clientId = clientId;
|
|
tokens.expires = genExpires(tokens.expires_in);
|
|
return tokens;
|
|
}
|
|
function fetchToken(hassUrl, clientId, code) {
|
|
return tokenRequest(hassUrl, clientId, {
|
|
code,
|
|
grant_type: "authorization_code"
|
|
});
|
|
}
|
|
function encodeOAuthState(state) {
|
|
return btoa(JSON.stringify(state));
|
|
}
|
|
function decodeOAuthState(encoded) {
|
|
return JSON.parse(atob(encoded));
|
|
}
|
|
var Auth = class {
|
|
constructor(data, saveTokens) {
|
|
this.data = data;
|
|
this._saveTokens = saveTokens;
|
|
}
|
|
get wsUrl() {
|
|
return `ws${this.data.hassUrl.substr(4)}/api/websocket`;
|
|
}
|
|
get accessToken() {
|
|
return this.data.access_token;
|
|
}
|
|
get expired() {
|
|
return Date.now() > this.data.expires;
|
|
}
|
|
/**
|
|
* Refresh the access token.
|
|
*/
|
|
async refreshAccessToken() {
|
|
if (!this.data.refresh_token)
|
|
throw new Error("No refresh_token");
|
|
const data = await tokenRequest(this.data.hassUrl, this.data.clientId, {
|
|
grant_type: "refresh_token",
|
|
refresh_token: this.data.refresh_token
|
|
});
|
|
data.refresh_token = this.data.refresh_token;
|
|
this.data = data;
|
|
if (this._saveTokens)
|
|
this._saveTokens(data);
|
|
}
|
|
/**
|
|
* Revoke the refresh & access tokens.
|
|
*/
|
|
async revoke() {
|
|
if (!this.data.refresh_token)
|
|
throw new Error("No refresh_token to revoke");
|
|
const formData = new FormData();
|
|
formData.append("token", this.data.refresh_token);
|
|
await fetch(`${this.data.hassUrl}/auth/revoke`, {
|
|
method: "POST",
|
|
credentials: "same-origin",
|
|
body: formData
|
|
});
|
|
if (this._saveTokens) {
|
|
this._saveTokens(null);
|
|
}
|
|
}
|
|
};
|
|
function createLongLivedTokenAuth(hassUrl, access_token) {
|
|
return new Auth({
|
|
hassUrl,
|
|
clientId: null,
|
|
expires: Date.now() + 1e11,
|
|
refresh_token: "",
|
|
access_token,
|
|
expires_in: 1e11
|
|
});
|
|
}
|
|
async function getAuth(options = {}) {
|
|
let data;
|
|
let hassUrl = options.hassUrl;
|
|
if (hassUrl && hassUrl[hassUrl.length - 1] === "/") {
|
|
hassUrl = hassUrl.substr(0, hassUrl.length - 1);
|
|
}
|
|
const clientId = options.clientId !== void 0 ? options.clientId : genClientId();
|
|
const limitHassInstance = options.limitHassInstance === true;
|
|
if (options.authCode && hassUrl) {
|
|
data = await fetchToken(hassUrl, clientId, options.authCode);
|
|
if (options.saveTokens) {
|
|
options.saveTokens(data);
|
|
}
|
|
}
|
|
if (!data) {
|
|
const query = parseQuery(location.search.substr(1));
|
|
if ("auth_callback" in query) {
|
|
const state = decodeOAuthState(query.state);
|
|
if (limitHassInstance && (state.hassUrl !== hassUrl || state.clientId !== clientId)) {
|
|
throw ERR_INVALID_AUTH_CALLBACK;
|
|
}
|
|
data = await fetchToken(state.hassUrl, state.clientId, query.code);
|
|
if (options.saveTokens) {
|
|
options.saveTokens(data);
|
|
}
|
|
}
|
|
}
|
|
if (!data && options.loadTokens) {
|
|
data = await options.loadTokens();
|
|
}
|
|
if (data && (hassUrl === void 0 || data.hassUrl === hassUrl)) {
|
|
return new Auth(data, options.saveTokens);
|
|
}
|
|
if (hassUrl === void 0) {
|
|
throw ERR_HASS_HOST_REQUIRED;
|
|
}
|
|
redirectAuthorize(hassUrl, clientId, options.redirectUrl || genRedirectUrl(), encodeOAuthState({
|
|
hassUrl,
|
|
clientId
|
|
}));
|
|
return new Promise(() => {
|
|
});
|
|
}
|
|
|
|
// node_modules/home-assistant-js-websocket/dist/store.js
|
|
var createStore = (state) => {
|
|
let listeners = [];
|
|
function unsubscribe(listener) {
|
|
let out = [];
|
|
for (let i = 0; i < listeners.length; i++) {
|
|
if (listeners[i] === listener) {
|
|
listener = null;
|
|
} else {
|
|
out.push(listeners[i]);
|
|
}
|
|
}
|
|
listeners = out;
|
|
}
|
|
function setState(update, overwrite) {
|
|
state = overwrite ? update : Object.assign(Object.assign({}, state), update);
|
|
let currentListeners = listeners;
|
|
for (let i = 0; i < currentListeners.length; i++) {
|
|
currentListeners[i](state);
|
|
}
|
|
}
|
|
return {
|
|
get state() {
|
|
return state;
|
|
},
|
|
/**
|
|
* Create a bound copy of the given action function.
|
|
* The bound returned function invokes action() and persists the result back to the store.
|
|
* If the return value of `action` is a Promise, the resolved value will be used as state.
|
|
* @param {Function} action An action of the form `action(state, ...args) -> stateUpdate`
|
|
* @returns {Function} boundAction()
|
|
*/
|
|
action(action) {
|
|
function apply(result) {
|
|
setState(result, false);
|
|
}
|
|
return function() {
|
|
let args = [state];
|
|
for (let i = 0; i < arguments.length; i++)
|
|
args.push(arguments[i]);
|
|
let ret = action.apply(this, args);
|
|
if (ret != null) {
|
|
return ret instanceof Promise ? ret.then(apply) : apply(ret);
|
|
}
|
|
};
|
|
},
|
|
/**
|
|
* Apply a partial state object to the current state, invoking registered listeners.
|
|
* @param {Object} update An object with properties to be merged into state
|
|
* @param {Boolean} [overwrite=false] If `true`, update will replace state instead of being merged into it
|
|
*/
|
|
setState,
|
|
clearState() {
|
|
state = void 0;
|
|
},
|
|
/**
|
|
* Register a listener function to be called whenever state is changed. Returns an `unsubscribe()` function.
|
|
* @param {Function} listener A function to call when state changes. Gets passed the new state.
|
|
* @returns {Function} unsubscribe()
|
|
*/
|
|
subscribe(listener) {
|
|
listeners.push(listener);
|
|
return () => {
|
|
unsubscribe(listener);
|
|
};
|
|
}
|
|
// /**
|
|
// * Remove a previously-registered listener function.
|
|
// * @param {Function} listener The callback previously passed to `subscribe()` that should be removed.
|
|
// * @function
|
|
// */
|
|
// unsubscribe,
|
|
};
|
|
};
|
|
|
|
// node_modules/home-assistant-js-websocket/dist/collection.js
|
|
var UNSUB_GRACE_PERIOD = 5e3;
|
|
var DEBUG3 = false;
|
|
var getCollection = (conn, key, fetchCollection, subscribeUpdates4, options = { unsubGrace: true }) => {
|
|
if (conn[key]) {
|
|
return conn[key];
|
|
}
|
|
let active = 0;
|
|
let unsubProm;
|
|
let unsubTimer;
|
|
let store = createStore();
|
|
const refresh = () => {
|
|
if (!fetchCollection) {
|
|
throw new Error("Collection does not support refresh");
|
|
}
|
|
return fetchCollection(conn).then((state) => store.setState(state, true));
|
|
};
|
|
const refreshSwallow = () => refresh().catch((err) => {
|
|
if (conn.connected) {
|
|
throw err;
|
|
}
|
|
});
|
|
const setupUpdateSubscription = () => {
|
|
if (unsubTimer !== void 0) {
|
|
if (DEBUG3) {
|
|
console.log(`Prevented unsubscribe for ${key}`);
|
|
}
|
|
clearTimeout(unsubTimer);
|
|
unsubTimer = void 0;
|
|
return;
|
|
}
|
|
if (DEBUG3) {
|
|
console.log(`Subscribing to ${key}`);
|
|
}
|
|
if (subscribeUpdates4) {
|
|
unsubProm = subscribeUpdates4(conn, store);
|
|
}
|
|
if (fetchCollection) {
|
|
conn.addEventListener("ready", refreshSwallow);
|
|
refreshSwallow();
|
|
}
|
|
conn.addEventListener("disconnected", handleDisconnect);
|
|
};
|
|
const teardownUpdateSubscription = () => {
|
|
if (DEBUG3) {
|
|
console.log(`Unsubscribing from ${key}`);
|
|
}
|
|
unsubTimer = void 0;
|
|
if (unsubProm)
|
|
unsubProm.then((unsub) => {
|
|
unsub();
|
|
});
|
|
store.clearState();
|
|
conn.removeEventListener("ready", refresh);
|
|
conn.removeEventListener("disconnected", handleDisconnect);
|
|
};
|
|
const scheduleTeardownUpdateSubscription = () => {
|
|
if (DEBUG3) {
|
|
console.log(`Scheduling unsubscribing from ${key}`);
|
|
}
|
|
unsubTimer = setTimeout(teardownUpdateSubscription, UNSUB_GRACE_PERIOD);
|
|
};
|
|
const handleDisconnect = () => {
|
|
if (unsubTimer) {
|
|
clearTimeout(unsubTimer);
|
|
teardownUpdateSubscription();
|
|
}
|
|
};
|
|
conn[key] = {
|
|
get state() {
|
|
return store.state;
|
|
},
|
|
refresh,
|
|
subscribe(subscriber) {
|
|
active++;
|
|
if (DEBUG3) {
|
|
console.log(`New subscriber for ${key}. Active subscribers: ${active}`);
|
|
}
|
|
if (active === 1) {
|
|
setupUpdateSubscription();
|
|
}
|
|
const unsub = store.subscribe(subscriber);
|
|
if (store.state !== void 0) {
|
|
setTimeout(() => subscriber(store.state), 0);
|
|
}
|
|
return () => {
|
|
unsub();
|
|
active--;
|
|
if (DEBUG3) {
|
|
console.log(`Unsubscribe for ${key}. Active subscribers: ${active}`);
|
|
}
|
|
if (!active) {
|
|
options.unsubGrace ? scheduleTeardownUpdateSubscription() : teardownUpdateSubscription();
|
|
}
|
|
};
|
|
}
|
|
};
|
|
return conn[key];
|
|
};
|
|
var createCollection = (key, fetchCollection, subscribeUpdates4, conn, onChange) => getCollection(conn, key, fetchCollection, subscribeUpdates4).subscribe(onChange);
|
|
|
|
// node_modules/home-assistant-js-websocket/dist/commands.js
|
|
var getStates = (connection) => connection.sendMessagePromise(states());
|
|
var getServices = (connection) => connection.sendMessagePromise(services());
|
|
var getConfig = (connection) => connection.sendMessagePromise(config());
|
|
var getUser = (connection) => connection.sendMessagePromise(user());
|
|
var callService2 = (connection, domain, service, serviceData, target, returnResponse) => connection.sendMessagePromise(callService(domain, service, serviceData, target, returnResponse));
|
|
|
|
// node_modules/home-assistant-js-websocket/dist/config.js
|
|
function processComponentLoaded(state, event) {
|
|
if (state === void 0)
|
|
return null;
|
|
return {
|
|
components: state.components.concat(event.data.component)
|
|
};
|
|
}
|
|
var fetchConfig = (conn) => getConfig(conn);
|
|
var subscribeUpdates = (conn, store) => Promise.all([
|
|
conn.subscribeEvents(store.action(processComponentLoaded), "component_loaded"),
|
|
conn.subscribeEvents(() => fetchConfig(conn).then((config2) => store.setState(config2, true)), "core_config_updated")
|
|
]).then((unsubs) => () => unsubs.forEach((unsub) => unsub()));
|
|
var configColl = (conn) => getCollection(conn, "_cnf", fetchConfig, subscribeUpdates);
|
|
var subscribeConfig = (conn, onChange) => configColl(conn).subscribe(onChange);
|
|
var STATE_NOT_RUNNING = "NOT_RUNNING";
|
|
var STATE_STARTING = "STARTING";
|
|
var STATE_RUNNING = "RUNNING";
|
|
var STATE_STOPPING = "STOPPING";
|
|
var STATE_FINAL_WRITE = "FINAL_WRITE";
|
|
|
|
// node_modules/home-assistant-js-websocket/dist/services.js
|
|
function processServiceRegistered(conn, store, event) {
|
|
var _a;
|
|
const state = store.state;
|
|
if (state === void 0)
|
|
return;
|
|
const { domain, service } = event.data;
|
|
if (!((_a = state.domain) === null || _a === void 0 ? void 0 : _a.service)) {
|
|
const domainInfo = Object.assign(Object.assign({}, state[domain]), { [service]: { description: "", fields: {} } });
|
|
store.setState({ [domain]: domainInfo });
|
|
}
|
|
debouncedFetchServices(conn, store);
|
|
}
|
|
function processServiceRemoved(state, event) {
|
|
if (state === void 0)
|
|
return null;
|
|
const { domain, service } = event.data;
|
|
const curDomainInfo = state[domain];
|
|
if (!curDomainInfo || !(service in curDomainInfo))
|
|
return null;
|
|
const domainInfo = {};
|
|
Object.keys(curDomainInfo).forEach((sKey) => {
|
|
if (sKey !== service)
|
|
domainInfo[sKey] = curDomainInfo[sKey];
|
|
});
|
|
return { [domain]: domainInfo };
|
|
}
|
|
var debouncedFetchServices = debounce((conn, store) => fetchServices(conn).then((services2) => store.setState(services2, true)), 5e3);
|
|
var fetchServices = (conn) => getServices(conn);
|
|
var subscribeUpdates2 = (conn, store) => Promise.all([
|
|
conn.subscribeEvents((ev) => processServiceRegistered(conn, store, ev), "service_registered"),
|
|
conn.subscribeEvents(store.action(processServiceRemoved), "service_removed")
|
|
]).then((unsubs) => () => unsubs.forEach((fn) => fn()));
|
|
var servicesColl = (conn) => getCollection(conn, "_srv", fetchServices, subscribeUpdates2);
|
|
var subscribeServices = (conn, onChange) => servicesColl(conn).subscribe(onChange);
|
|
|
|
// node_modules/home-assistant-js-websocket/dist/entities.js
|
|
function processEvent(store, updates) {
|
|
const state = Object.assign({}, store.state);
|
|
if (updates.a) {
|
|
for (const entityId in updates.a) {
|
|
const newState = updates.a[entityId];
|
|
let last_changed = new Date(newState.lc * 1e3).toISOString();
|
|
state[entityId] = {
|
|
entity_id: entityId,
|
|
state: newState.s,
|
|
attributes: newState.a,
|
|
context: typeof newState.c === "string" ? { id: newState.c, parent_id: null, user_id: null } : newState.c,
|
|
last_changed,
|
|
last_updated: newState.lu ? new Date(newState.lu * 1e3).toISOString() : last_changed
|
|
};
|
|
}
|
|
}
|
|
if (updates.r) {
|
|
for (const entityId of updates.r) {
|
|
delete state[entityId];
|
|
}
|
|
}
|
|
if (updates.c) {
|
|
for (const entityId in updates.c) {
|
|
let entityState = state[entityId];
|
|
if (!entityState) {
|
|
console.warn("Received state update for unknown entity", entityId);
|
|
continue;
|
|
}
|
|
entityState = Object.assign({}, entityState);
|
|
const { "+": toAdd, "-": toRemove } = updates.c[entityId];
|
|
const attributesChanged = (toAdd === null || toAdd === void 0 ? void 0 : toAdd.a) || (toRemove === null || toRemove === void 0 ? void 0 : toRemove.a);
|
|
const attributes = attributesChanged ? Object.assign({}, entityState.attributes) : entityState.attributes;
|
|
if (toAdd) {
|
|
if (toAdd.s !== void 0) {
|
|
entityState.state = toAdd.s;
|
|
}
|
|
if (toAdd.c) {
|
|
if (typeof toAdd.c === "string") {
|
|
entityState.context = Object.assign(Object.assign({}, entityState.context), { id: toAdd.c });
|
|
} else {
|
|
entityState.context = Object.assign(Object.assign({}, entityState.context), toAdd.c);
|
|
}
|
|
}
|
|
if (toAdd.lc) {
|
|
entityState.last_updated = entityState.last_changed = new Date(toAdd.lc * 1e3).toISOString();
|
|
} else if (toAdd.lu) {
|
|
entityState.last_updated = new Date(toAdd.lu * 1e3).toISOString();
|
|
}
|
|
if (toAdd.a) {
|
|
Object.assign(attributes, toAdd.a);
|
|
}
|
|
}
|
|
if (toRemove === null || toRemove === void 0 ? void 0 : toRemove.a) {
|
|
for (const key of toRemove.a) {
|
|
delete attributes[key];
|
|
}
|
|
}
|
|
if (attributesChanged) {
|
|
entityState.attributes = attributes;
|
|
}
|
|
state[entityId] = entityState;
|
|
}
|
|
}
|
|
store.setState(state, true);
|
|
}
|
|
var subscribeUpdates3 = (conn, store) => conn.subscribeMessage((ev) => processEvent(store, ev), {
|
|
type: "subscribe_entities"
|
|
});
|
|
function legacyProcessEvent(store, event) {
|
|
const state = store.state;
|
|
if (state === void 0)
|
|
return;
|
|
const { entity_id, new_state } = event.data;
|
|
if (new_state) {
|
|
store.setState({ [new_state.entity_id]: new_state });
|
|
} else {
|
|
const newEntities = Object.assign({}, state);
|
|
delete newEntities[entity_id];
|
|
store.setState(newEntities, true);
|
|
}
|
|
}
|
|
async function legacyFetchEntities(conn) {
|
|
const states2 = await getStates(conn);
|
|
const entities = {};
|
|
for (let i = 0; i < states2.length; i++) {
|
|
const state = states2[i];
|
|
entities[state.entity_id] = state;
|
|
}
|
|
return entities;
|
|
}
|
|
var legacySubscribeUpdates = (conn, store) => conn.subscribeEvents((ev) => legacyProcessEvent(store, ev), "state_changed");
|
|
var entitiesColl = (conn) => atLeastHaVersion(conn.haVersion, 2022, 4, 0) ? getCollection(conn, "_ent", void 0, subscribeUpdates3) : getCollection(conn, "_ent", legacyFetchEntities, legacySubscribeUpdates);
|
|
var subscribeEntities = (conn, onChange) => entitiesColl(conn).subscribe(onChange);
|
|
|
|
// node_modules/home-assistant-js-websocket/dist/index.js
|
|
async function createConnection(options) {
|
|
const connOptions = Object.assign({ setupRetry: 0, createSocket }, options);
|
|
const socket = await connOptions.createSocket(connOptions);
|
|
const conn = new Connection(socket, connOptions);
|
|
return conn;
|
|
}
|
|
export {
|
|
Auth,
|
|
Connection,
|
|
ERR_CANNOT_CONNECT,
|
|
ERR_CONNECTION_LOST,
|
|
ERR_HASS_HOST_REQUIRED,
|
|
ERR_INVALID_AUTH,
|
|
ERR_INVALID_AUTH_CALLBACK,
|
|
ERR_INVALID_HTTPS_TO_HTTP,
|
|
MSG_TYPE_AUTH_INVALID,
|
|
MSG_TYPE_AUTH_OK,
|
|
MSG_TYPE_AUTH_REQUIRED,
|
|
STATE_FINAL_WRITE,
|
|
STATE_NOT_RUNNING,
|
|
STATE_RUNNING,
|
|
STATE_STARTING,
|
|
STATE_STOPPING,
|
|
callService2 as callService,
|
|
configColl,
|
|
createCollection,
|
|
createConnection,
|
|
createLongLivedTokenAuth,
|
|
createSocket,
|
|
createStore,
|
|
entitiesColl,
|
|
genClientId,
|
|
genExpires,
|
|
getAuth,
|
|
getCollection,
|
|
getConfig,
|
|
getServices,
|
|
getStates,
|
|
getUser,
|
|
servicesColl,
|
|
subscribeConfig,
|
|
subscribeEntities,
|
|
subscribeServices
|
|
};
|
|
//# sourceMappingURL=home-assistant-js-websocket.js.map
|