Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

home-assistant-js-websocket

Package Overview
Dependencies
Maintainers
1
Versions
102
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

home-assistant-js-websocket - npm Package Compare versions

Comparing version 4.5.0 to 5.0.0

dist/auth.js

0

dist/auth.d.ts

@@ -0,0 +0,0 @@ export declare type AuthData = {

6

dist/collection.d.ts

@@ -1,4 +0,4 @@

import { Store } from "./store";
import { Connection } from "./connection";
import { UnsubscribeFunc } from "./types";
import { Store } from "./store.js";
import { Connection } from "./connection.js";
import { UnsubscribeFunc } from "./types.js";
export declare type Collection<State> = {

@@ -5,0 +5,0 @@ state: State;

@@ -1,3 +0,3 @@

import { Connection } from "./connection";
import { HassEntity, HassServices, HassConfig, HassUser } from "./types";
import { Connection } from "./connection.js";
import { HassEntity, HassServices, HassConfig, HassUser } from "./types.js";
export declare const getStates: (connection: Connection) => Promise<HassEntity[]>;

@@ -4,0 +4,0 @@ export declare const getServices: (connection: Connection) => Promise<HassServices>;

@@ -1,3 +0,3 @@

import { HassConfig, UnsubscribeFunc } from "./types";
import { Connection } from "./connection";
import { HassConfig, UnsubscribeFunc } from "./types.js";
import { Connection } from "./connection.js";
export declare const subscribeConfig: (conn: Connection, onChange: (state: HassConfig) => void) => UnsubscribeFunc;

@@ -1,3 +0,3 @@

import { ConnectionOptions, MessageBase } from "./types";
import { HaWebSocket } from "./socket";
import { ConnectionOptions, MessageBase } from "./types.js";
import { HaWebSocket } from "./socket.js";
export declare type ConnectionEventListener = (conn: Connection, eventData?: any) => void;

@@ -4,0 +4,0 @@ declare type Events = "ready" | "disconnected" | "reconnect-error";

@@ -1,4 +0,4 @@

import { HassEntities, UnsubscribeFunc } from "./types";
import { Connection } from "./connection";
export declare const entitiesColl: (conn: Connection) => import("./collection").Collection<HassEntities>;
import { HassEntities, UnsubscribeFunc } from "./types.js";
import { Connection } from "./connection.js";
export declare const entitiesColl: (conn: Connection) => import("./collection.js").Collection<HassEntities>;
export declare const subscribeEntities: (conn: Connection, onChange: (state: HassEntities) => void) => UnsubscribeFunc;

@@ -0,0 +0,0 @@ export declare const ERR_CANNOT_CONNECT = 1;

@@ -1,2 +0,801 @@

!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t(e.HAWS={})}(this,function(e){function t(e,t){try{var n=e()}catch(e){return t(e)}return n&&n.then?n.then(void 0,t):n}"undefined"!=typeof Symbol&&(Symbol.iterator||(Symbol.iterator=Symbol("Symbol.iterator"))),"undefined"!=typeof Symbol&&(Symbol.asyncIterator||(Symbol.asyncIterator=Symbol("Symbol.asyncIterator")));var n=4;function r(e){return{type:"unsubscribe_events",subscription:e}}var o=function(e,t){this.options=t,this.commandId=1,this.commands=new Map,this.eventListeners=new Map,this.closeRequested=!1,this.setSocket(e)},s={haVersion:{configurable:!0}};s.haVersion.get=function(){return this.socket.haVersion},o.prototype.setSocket=function(e){var t=this,n=this.socket;if(this.socket=e,e.addEventListener("message",function(e){return t._handleMessage(e)}),e.addEventListener("close",function(e){return t._handleClose(e)}),n){var r=this.commands;this.commandId=1,this.commands=new Map,r.forEach(function(e){"subscribe"in e&&e.subscribe().then(function(t){e.unsubscribe=t,e.resolve()})}),this.fireEvent("ready")}},o.prototype.addEventListener=function(e,t){var n=this.eventListeners.get(e);n||this.eventListeners.set(e,n=[]),n.push(t)},o.prototype.removeEventListener=function(e,t){var n=this.eventListeners.get(e);if(n){var r=n.indexOf(t);-1!==r&&n.splice(r,1)}},o.prototype.fireEvent=function(e,t){var n=this;(this.eventListeners.get(e)||[]).forEach(function(e){return e(n,t)})},o.prototype.close=function(){this.closeRequested=!0,this.socket.close()},o.prototype.subscribeEvents=function(e,t){try{return Promise.resolve(this.subscribeMessage(e,function(e){var t={type:"subscribe_events"};return e&&(t.event_type=e),t}(t)))}catch(e){return Promise.reject(e)}},o.prototype.ping=function(){return this.sendMessagePromise({type:"ping"})},o.prototype.sendMessage=function(e,t){t||(t=this._genCmdId()),e.id=t,this.socket.send(JSON.stringify(e))},o.prototype.sendMessagePromise=function(e){var t=this;return new Promise(function(n,r){var o=t._genCmdId();t.commands.set(o,{resolve:n,reject:r}),t.sendMessage(e,o)})},o.prototype.subscribeMessage=function(e,t){try{var n,o=this,s=o._genCmdId();return Promise.resolve(new Promise(function(i,c){n={resolve:i,reject:c,callback:e,subscribe:function(){return o.subscribeMessage(e,t)},unsubscribe:function(){try{return Promise.resolve(o.sendMessagePromise(r(s))).then(function(){o.commands.delete(s)})}catch(e){return Promise.reject(e)}}},o.commands.set(s,n);try{o.sendMessage(t,s)}catch(e){}})).then(function(){return function(){return n.unsubscribe()}})}catch(e){return Promise.reject(e)}},o.prototype._handleMessage=function(e){var t=JSON.parse(e.data),n=this.commands.get(t.id);switch(t.type){case"event":n?n.callback(t.event):(console.warn("Received event for unknown subscription "+t.id+". Unsubscribing."),this.sendMessagePromise(r(t.id)));break;case"result":n&&(t.success?(n.resolve(t.result),"subscribe"in n||this.commands.delete(t.id)):(n.reject(t.error),this.commands.delete(t.id)));break;case"pong":n?(n.resolve(),this.commands.delete(t.id)):console.warn("Received unknown pong response "+t.id)}},o.prototype._handleClose=function(e){var n=this;if(this.commands.forEach(function(e){"subscribe"in e||e.reject({type:"result",success:!1,error:{code:3,message:"Connection lost"}})}),!this.closeRequested){this.fireEvent("disconnected");var r=Object.assign({},this.options,{setupRetry:0}),o=function(e){var s=n;setTimeout(function(){try{var n=t(function(){return Promise.resolve(r.createSocket(r)).then(function(e){s.setSocket(e)})},function(t){2===t?s.fireEvent("reconnect-error",t):o(e+1)});return Promise.resolve(n&&n.then?n.then(function(){}):void 0)}catch(e){return Promise.reject(e)}},1e3*Math.min(e,5))};o(0)}},o.prototype._genCmdId=function(){return++this.commandId},Object.defineProperties(o.prototype,s);var i=function(e,t,n){try{var r="undefined"!=typeof location&&location;if(r&&"https:"===r.protocol){var o=document.createElement("a");if(o.href=e,"http:"===o.protocol&&"localhost"!==o.hostname)throw 5}var s=new FormData;return null!==t&&s.append("client_id",t),Object.keys(n).forEach(function(e){s.append(e,n[e])}),Promise.resolve(fetch(e+"/auth/token",{method:"POST",credentials:"same-origin",body:s})).then(function(n){if(!n.ok)throw 400===n.status||403===n.status?2:new Error("Unable to fetch tokens");return Promise.resolve(n.json()).then(function(n){return n.hassUrl=e,n.clientId=t,n.expires=u(n.expires_in),n})})}catch(e){return Promise.reject(e)}},c=function(){return location.protocol+"//"+location.host+"/"},u=function(e){return 1e3*e+Date.now()};function a(e,t,n){return i(e,t,{code:n,grant_type:"authorization_code"})}var f=function(e,t){this.data=e,this._saveTokens=t},d={wsUrl:{configurable:!0},accessToken:{configurable:!0},expired:{configurable:!0}};d.wsUrl.get=function(){return"ws"+this.data.hassUrl.substr(4)+"/api/websocket"},d.accessToken.get=function(){return this.data.access_token},d.expired.get=function(){return Date.now()>this.data.expires},f.prototype.refreshAccessToken=function(){try{var e=this;return Promise.resolve(i(e.data.hassUrl,e.data.clientId,{grant_type:"refresh_token",refresh_token:e.data.refresh_token})).then(function(t){t.refresh_token=e.data.refresh_token,e.data=t,e._saveTokens&&e._saveTokens(t)})}catch(e){return Promise.reject(e)}},f.prototype.revoke=function(){try{var e=this,t=new FormData;return t.append("action","revoke"),t.append("token",e.data.refresh_token),Promise.resolve(fetch(e.data.hassUrl+"/auth/token",{method:"POST",credentials:"same-origin",body:t})).then(function(){e._saveTokens&&e._saveTokens(null)})}catch(e){return Promise.reject(e)}},Object.defineProperties(f.prototype,d);var h=function(e,t,n,r){if(e[t])return e[t];var o,s=0,i=function(e){var t=[];function n(n,r){e=r?n:Object.assign({},e,n);for(var o=t,s=0;s<o.length;s++)o[s](e)}return{get state(){return e},action:function(t){function r(e){n(e,!1)}return function(){for(var n=arguments,o=[e],s=0;s<arguments.length;s++)o.push(n[s]);var i=t.apply(this,o);if(null!=i)return i.then?i.then(r):r(i)}},setState:n,subscribe:function(e){return t.push(e),function(){!function(e){for(var n=[],r=0;r<t.length;r++)t[r]===e?e=null:n.push(t[r]);t=n}(e)}}}}(),c=function(){return n(e).then(function(e){return i.setState(e,!0)})},u=function(){return c().catch(function(t){if(e.socket.readyState==e.socket.OPEN)throw t})};return e[t]={get state(){return i.state},refresh:c,subscribe:function(t){1==++s&&(r&&(o=r(e,i)),e.addEventListener("ready",u),u());var n=i.subscribe(t);return void 0!==i.state&&setTimeout(function(){return t(i.state)},0),function(){n(),--s||(o&&o.then(function(e){e()}),e.removeEventListener("ready",c))}}},e[t]},v=function(e){return e.sendMessagePromise({type:"get_states"})},l=function(e){return e.sendMessagePromise({type:"get_services"})},m=function(e){return e.sendMessagePromise({type:"get_config"})};function p(e,t){return void 0===e?null:{components:e.components.concat(t.data.component)}}var b=function(e){return m(e)},y=function(e,t){return Promise.all([e.subscribeEvents(t.action(p),"component_loaded"),e.subscribeEvents(function(){return b(e).then(function(e){return t.setState(e,!0)})},"core_config_updated")]).then(function(e){return function(){return e.forEach(function(e){return e()})}})};function g(e,t){var n,r;if(void 0===e)return null;var o=t.data,s=o.domain,i=Object.assign({},e[s],((n={})[o.service]={description:"",fields:{}},n));return(r={})[s]=i,r}function _(e,t){var n;if(void 0===e)return null;var r=t.data,o=r.domain,s=r.service,i=e[o];if(!(i&&s in i))return null;var c={};return Object.keys(i).forEach(function(e){e!==s&&(c[e]=i[e])}),(n={})[o]=c,n}var k=function(e){return l(e)},P=function(e,t){return Promise.all([e.subscribeEvents(t.action(g),"service_registered"),e.subscribeEvents(t.action(_),"service_removed")]).then(function(e){return function(){return e.forEach(function(e){return e()})}})},E=function(e){try{return Promise.resolve(v(e)).then(function(e){for(var t={},n=0;n<e.length;n++){var r=e[n];t[r.entity_id]=r}return t})}catch(e){return Promise.reject(e)}},S=function(e,t){return e.subscribeEvents(function(e){return function(t,n){var r,o=t.state;if(void 0!==o){var s=e.data,i=s.entity_id,c=s.new_state;if(c)t.setState(((r={})[c.entity_id]=c,r));else{var u=Object.assign({},o);delete u[i],t.setState(u,!0)}}}(t)},"state_changed")},T=function(e){return h(e,"_ent",E,S)},w={setupRetry:0,createSocket:function(e){if(!e.auth)throw n;var r=e.auth,o=r.expired?r.refreshAccessToken().then(function(){o=void 0},function(){o=void 0}):void 0,s=r.wsUrl;return new Promise(function(n,i){return function e(n,i,c){var u=new WebSocket(s),a=!1,f=function(){if(u.removeEventListener("close",f),a)c(2);else if(0!==n){var t=-1===n?-1:n-1;setTimeout(function(){return e(t,i,c)},1e3)}else c(1)},d=function(e){try{var n=t(function(){function e(){u.send(JSON.stringify({type:"auth",access_token:r.accessToken}))}var t=function(){if(r.expired)return Promise.resolve(o||r.refreshAccessToken()).then(function(){})}();return t&&t.then?t.then(e):e()},function(e){a=2===e,u.close()});return Promise.resolve(n&&n.then?n.then(function(){}):void 0)}catch(e){return Promise.reject(e)}},h=function(e){try{var t=JSON.parse(e.data);switch(t.type){case"auth_invalid":a=!0,u.close();break;case"auth_ok":u.removeEventListener("open",d),u.removeEventListener("message",h),u.removeEventListener("close",f),u.removeEventListener("error",f),u.haVersion=t.ha_version,i(u)}return Promise.resolve()}catch(e){return Promise.reject(e)}};u.addEventListener("open",d),u.addEventListener("message",h),u.addEventListener("close",f),u.addEventListener("error",f)}(e.setupRetry,n,i)})}};e.createConnection=function(e){try{var t=Object.assign({},w,e);return Promise.resolve(t.createSocket(t)).then(function(e){return new o(e,t)})}catch(e){return Promise.reject(e)}},e.getAuth=function(e){void 0===e&&(e={});try{function t(){function t(){function t(){if(r)return new f(r,e.saveTokens);if(void 0===o)throw n;return function(e,t,n,r){n+=(n.includes("?")?"&":"?")+"auth_callback=1",document.location.href=function(e,t,n,r){var o=e+"/auth/authorize?response_type=code&redirect_uri="+encodeURIComponent(n);return null!==t&&(o+="&client_id="+encodeURIComponent(t)),r&&(o+="&state="+encodeURIComponent(r)),o}(e,t,n,r)}(o,s,e.redirectUrl||location.protocol+"//"+location.host+location.pathname+location.search,btoa(JSON.stringify({hassUrl:o,clientId:s}))),new Promise(function(){})}var i=function(){if(!r&&e.loadTokens)return Promise.resolve(e.loadTokens()).then(function(e){r=e})}();return i&&i.then?i.then(t):t()}var i=function(){if(!r){var t=function(e){for(var t={},n=location.search.substr(1).split("&"),r=0;r<n.length;r++){var o=n[r].split("="),s=decodeURIComponent(o[0]),i=o.length>1?decodeURIComponent(o[1]):void 0;t[s]=i}return t}(),n=function(){if("auth_callback"in t){var n=JSON.parse(atob(t.state));return Promise.resolve(a(n.hassUrl,n.clientId,t.code)).then(function(t){r=t,e.saveTokens&&e.saveTokens(r)})}}();if(n&&n.then)return n.then(function(){})}}();return i&&i.then?i.then(t):t()}var r,o=e.hassUrl;o&&"/"===o[o.length-1]&&(o=o.substr(0,o.length-1));var s=void 0!==e.clientId?e.clientId:c(),i=function(){if(!r&&e.authCode&&o)return Promise.resolve(a(o,s,e.authCode)).then(function(t){r=t,e.saveTokens&&e.saveTokens(r)})}();return Promise.resolve(i&&i.then?i.then(t):t())}catch(e){return Promise.reject(e)}},e.genClientId=c,e.genExpires=u,e.Auth=f,e.getCollection=h,e.createCollection=function(e,t,n,r,o){return h(r,e,t,n).subscribe(o)},e.Connection=o,e.subscribeConfig=function(e,t){return function(e){return h(e,"_cnf",b,y)}(e).subscribe(t)},e.subscribeServices=function(e,t){return function(e){return h(e,"_srv",k,P)}(e).subscribe(t)},e.entitiesColl=T,e.subscribeEntities=function(e,t){return T(e).subscribe(t)},e.ERR_CANNOT_CONNECT=1,e.ERR_INVALID_AUTH=2,e.ERR_CONNECTION_LOST=3,e.ERR_HASS_HOST_REQUIRED=n,e.ERR_INVALID_HTTPS_TO_HTTP=5,e.getStates=v,e.getServices=l,e.getConfig=m,e.getUser=function(e){return e.sendMessagePromise({type:"auth/current_user"})},e.callService=function(e,t,n,r){return e.sendMessagePromise(function(e,t,n){var r={type:"call_service",domain:e,service:t};return n&&(r.service_data=n),r}(t,n,r))}});
//# sourceMappingURL=haws.umd.js.map
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
typeof define === 'function' && define.amd ? define(['exports'], factory) :
(global = global || self, factory(global.HAWS = {}));
}(this, (function (exports) { 'use strict';
const ERR_CANNOT_CONNECT = 1;
const ERR_INVALID_AUTH = 2;
const ERR_CONNECTION_LOST = 3;
const ERR_HASS_HOST_REQUIRED = 4;
const ERR_INVALID_HTTPS_TO_HTTP = 5;
function auth(accessToken) {
return {
type: "auth",
access_token: accessToken
};
}
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) {
const message = {
type: "call_service",
domain,
service
};
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
}
};
}
/**
* Create a web socket connection with a Home Assistant instance.
*/
const MSG_TYPE_AUTH_INVALID = "auth_invalid";
const MSG_TYPE_AUTH_OK = "auth_ok";
function createSocket(options) {
if (!options.auth) {
throw ERR_HASS_HOST_REQUIRED;
}
const auth$1 = options.auth;
// Start refreshing expired tokens even before the WS connection is open.
// We know that we will need auth anyway.
let authRefreshTask = auth$1.expired
? auth$1.refreshAccessToken().then(() => {
authRefreshTask = undefined;
}, () => {
authRefreshTask = undefined;
})
: undefined;
// Convert from http:// -> ws://, https:// -> wss://
const url = auth$1.wsUrl;
function connect(triesLeft, promResolve, promReject) {
const socket = new WebSocket(url);
// If invalid auth, we will not try to reconnect.
let invalidAuth = false;
const closeMessage = () => {
// If we are in error handler make sure close handler doesn't also fire.
socket.removeEventListener("close", closeMessage);
if (invalidAuth) {
promReject(ERR_INVALID_AUTH);
return;
}
// Reject if we no longer have to retry
if (triesLeft === 0) {
// We never were connected and will not retry
promReject(ERR_CANNOT_CONNECT);
return;
}
const newTries = triesLeft === -1 ? -1 : triesLeft - 1;
// Try again in a second
setTimeout(() => connect(newTries, promResolve, promReject), 1000);
};
// Auth is mandatory, so we can send the auth message right away.
const handleOpen = async (event) => {
try {
if (auth$1.expired) {
await (authRefreshTask ? authRefreshTask : auth$1.refreshAccessToken());
}
socket.send(JSON.stringify(auth(auth$1.accessToken)));
}
catch (err) {
// Refresh token failed
invalidAuth = err === ERR_INVALID_AUTH;
socket.close();
}
};
const handleMessage = async (event) => {
const message = JSON.parse(event.data);
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;
promResolve(socket);
break;
}
};
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));
}
/**
* Connection that wraps a socket and provides an interface to interact with
* the Home Assistant websocket API.
*/
class Connection {
constructor(socket, options) {
// connection options
// - setupRetry: amount of ms to retry when unable to connect on initial setup
// - createSocket: create a new Socket connection
this.options = options;
// id if next command to send
this.commandId = 1;
// info about active subscriptions and commands in flight
this.commands = new Map();
// map of event listeners
this.eventListeners = new Map();
// true if a close is requested by the user
this.closeRequested = false;
this.setSocket(socket);
}
get haVersion() {
return this.socket.haVersion;
}
setSocket(socket) {
const oldSocket = this.socket;
this.socket = socket;
socket.addEventListener("message", ev => this._handleMessage(ev));
socket.addEventListener("close", ev => this._handleClose(ev));
if (oldSocket) {
const oldCommands = this.commands;
// reset to original state
this.commandId = 1;
this.commands = new Map();
oldCommands.forEach(info => {
if ("subscribe" in info) {
info.subscribe().then(unsub => {
info.unsubscribe = unsub;
// We need to resolve this in case it wasn't resolved yet.
// This allows us to subscribe while we're disconnected
// and recover properly.
info.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));
}
close() {
this.closeRequested = true;
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 (!commandId) {
commandId = this._genCmdId();
}
message.id = commandId;
this.socket.send(JSON.stringify(message));
}
sendMessagePromise(message) {
return new Promise((resolve, reject) => {
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
* @returns promise that resolves to an unsubscribe function
*/
async subscribeMessage(callback, subscribeMessage) {
// Command ID that will be used
const commandId = this._genCmdId();
let info;
await new Promise((resolve, reject) => {
// We store unsubscribe on info object. That way we can overwrite it in case
// we get disconnected and we have to subscribe again.
info = {
resolve,
reject,
callback,
subscribe: () => this.subscribeMessage(callback, subscribeMessage),
unsubscribe: async () => {
await this.sendMessagePromise(unsubscribeEvents(commandId));
this.commands.delete(commandId);
}
};
this.commands.set(commandId, info);
try {
this.sendMessage(subscribeMessage, commandId);
}
catch (err) {
// Happens when the websocket is already closing.
// Don't have to handle the error, reconnect logic will pick it up.
}
});
return () => info.unsubscribe();
}
_handleMessage(event) {
const message = JSON.parse(event.data);
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));
}
break;
case "result":
// No info is fine. If just sendMessage is used, we did not store promise for result
if (info) {
if (message.success) {
info.resolve(message.result);
// Don't remove subscriptions.
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;
}
}
_handleClose(ev) {
// Reject in-flight sendMessagePromise requests
this.commands.forEach(info => {
// We don't cancel subscribeEvents commands in flight
// as we will be able to recover them.
if (!("subscribe" in info)) {
info.reject(error(ERR_CONNECTION_LOST, "Connection lost"));
}
});
if (this.closeRequested) {
return;
}
this.fireEvent("disconnected");
// Disable setupRetry, we control it here with auto-backoff
const options = Object.assign(Object.assign({}, this.options), { setupRetry: 0 });
const reconnect = (tries) => {
setTimeout(async () => {
try {
const socket = await options.createSocket(options);
this.setSocket(socket);
}
catch (err) {
if (err === ERR_INVALID_AUTH) {
this.fireEvent("reconnect-error", err);
}
else {
reconnect(tries + 1);
}
}
}, Math.min(tries, 5) * 1000);
};
reconnect(0);
}
_genCmdId() {
return ++this.commandId;
}
}
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]) : undefined;
query[key] = value;
}
return query;
}
const genClientId = () => `${location.protocol}//${location.host}/`;
const genExpires = (expires_in) => {
return expires_in * 1000 + Date.now();
};
function genRedirectUrl() {
// Get current url but without # part.
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) {
// Add either ?auth_callback=1 or &auth_callback=1
redirectUrl += (redirectUrl.includes("?") ? "&" : "?") + "auth_callback=1";
document.location.href = genAuthorizeUrl(hassUrl, clientId, redirectUrl, state);
}
async function tokenRequest(hassUrl, clientId, data) {
// Browsers don't allow fetching tokens from https -> http.
// Throw an error because it's a pain to debug this.
// Guard against not working in node.
const l = typeof location !== "undefined" && location;
if (l && l.protocol === "https:") {
// Ensure that the hassUrl is hosted on 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 /* auth invalid */ ||
resp.status === 403 /* user not active */
? 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));
}
class Auth {
constructor(data, saveTokens) {
this.data = data;
this._saveTokens = saveTokens;
}
get wsUrl() {
// Convert from http:// -> ws://, https:// -> wss://
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() {
const data = await tokenRequest(this.data.hassUrl, this.data.clientId, {
grant_type: "refresh_token",
refresh_token: this.data.refresh_token
});
// Access token response does not contain 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() {
const formData = new FormData();
formData.append("action", "revoke");
formData.append("token", this.data.refresh_token);
// There is no error checking, as revoke will always return 200
await fetch(`${this.data.hassUrl}/auth/token`, {
method: "POST",
credentials: "same-origin",
body: formData
});
if (this._saveTokens) {
this._saveTokens(null);
}
}
}
async function getAuth(options = {}) {
let data;
let hassUrl = options.hassUrl;
// Strip trailing slash.
if (hassUrl && hassUrl[hassUrl.length - 1] === "/") {
hassUrl = hassUrl.substr(0, hassUrl.length - 1);
}
const clientId = options.clientId !== undefined ? options.clientId : genClientId();
// Use auth code if it was passed in
if (!data && options.authCode && hassUrl) {
data = await fetchToken(hassUrl, clientId, options.authCode);
if (options.saveTokens) {
options.saveTokens(data);
}
}
// Check if we came back from an authorize redirect
if (!data) {
const query = parseQuery(location.search.substr(1));
// Check if we got redirected here from authorize page
if ("auth_callback" in query) {
// Restore state
const state = decodeOAuthState(query.state);
data = await fetchToken(state.hassUrl, state.clientId, query.code);
if (options.saveTokens) {
options.saveTokens(data);
}
}
}
// Check for stored tokens
if (!data && options.loadTokens) {
data = await options.loadTokens();
}
if (data) {
return new Auth(data, options.saveTokens);
}
if (hassUrl === undefined) {
throw ERR_HASS_HOST_REQUIRED;
}
// If no tokens found but a hassUrl was passed in, let's go get some tokens!
redirectAuthorize(hassUrl, clientId, options.redirectUrl || genRedirectUrl(), encodeOAuthState({
hassUrl,
clientId
}));
// Just don't resolve while we navigate to next page
return new Promise(() => { });
}
const 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);
}
}
/**
* An observable state container, returned from {@link createStore}
* @name store
*/
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);
}
// Note: perf tests verifying this implementation: https://esbench.com/bench/5a295e6299634800a0349500
return function () {
let args = [state];
for (let i = 0; i < arguments.length; i++)
args.push(arguments[i]);
// @ts-ignore
let ret = action.apply(this, args);
if (ret != null) {
if (ret.then)
return ret.then(apply);
return 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,
/**
* 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,
};
};
const getCollection = (conn, key, fetchCollection, subscribeUpdates) => {
if (conn[key]) {
return conn[key];
}
let active = 0;
let unsubProm;
let store = createStore();
const refresh = () => fetchCollection(conn).then(state => store.setState(state, true));
const refreshSwallow = () => refresh().catch((err) => {
// Swallow errors if socket is connecting, closing or closed.
// We will automatically call refresh again when we re-establish the connection.
// Using conn.socket.OPEN instead of WebSocket for better node support
if (conn.socket.readyState == conn.socket.OPEN) {
throw err;
}
});
conn[key] = {
get state() {
return store.state;
},
refresh,
subscribe(subscriber) {
active++;
// If this was the first subscriber, attach collection
if (active === 1) {
if (subscribeUpdates) {
unsubProm = subscribeUpdates(conn, store);
}
// Fetch when connection re-established.
conn.addEventListener("ready", refreshSwallow);
refreshSwallow();
}
const unsub = store.subscribe(subscriber);
if (store.state !== undefined) {
// Don't call it right away so that caller has time
// to initialize all the things.
setTimeout(() => subscriber(store.state), 0);
}
return () => {
unsub();
active--;
if (!active) {
// Unsubscribe from changes
if (unsubProm)
unsubProm.then(unsub => {
unsub();
});
conn.removeEventListener("ready", refresh);
}
};
}
};
return conn[key];
};
// Legacy name. It gets a collection and subscribes.
const createCollection = (key, fetchCollection, subscribeUpdates, conn, onChange) => getCollection(conn, key, fetchCollection, subscribeUpdates).subscribe(onChange);
const getStates = (connection) => connection.sendMessagePromise(states());
const getServices = (connection) => connection.sendMessagePromise(services());
const getConfig = (connection) => connection.sendMessagePromise(config());
const getUser = (connection) => connection.sendMessagePromise(user());
const callService$1 = (connection, domain, service, serviceData) => connection.sendMessagePromise(callService(domain, service, serviceData));
function processComponentLoaded(state, event) {
if (state === undefined)
return null;
return {
components: state.components.concat(event.data.component)
};
}
const fetchConfig = (conn) => getConfig(conn);
const subscribeUpdates = (conn, store) => Promise.all([
conn.subscribeEvents(store.action(processComponentLoaded), "component_loaded"),
conn.subscribeEvents(() => fetchConfig(conn).then(config => store.setState(config, true)), "core_config_updated")
]).then(unsubs => () => unsubs.forEach(unsub => unsub()));
const configColl = (conn) => getCollection(conn, "_cnf", fetchConfig, subscribeUpdates);
const subscribeConfig = (conn, onChange) => configColl(conn).subscribe(onChange);
function processServiceRegistered(state, event) {
if (state === undefined)
return null;
const { domain, service } = event.data;
const domainInfo = Object.assign({}, state[domain], {
[service]: { description: "", fields: {} }
});
return { [domain]: domainInfo };
}
function processServiceRemoved(state, event) {
if (state === undefined)
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 };
}
const fetchServices = (conn) => getServices(conn);
const subscribeUpdates$1 = (conn, store) => Promise.all([
conn.subscribeEvents(store.action(processServiceRegistered), "service_registered"),
conn.subscribeEvents(store.action(processServiceRemoved), "service_removed")
]).then(unsubs => () => unsubs.forEach(fn => fn()));
const servicesColl = (conn) => getCollection(conn, "_srv", fetchServices, subscribeUpdates$1);
const subscribeServices = (conn, onChange) => servicesColl(conn).subscribe(onChange);
function processEvent(store, event) {
const state = store.state;
if (state === undefined)
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 fetchEntities(conn) {
const states = await getStates(conn);
const entities = {};
for (let i = 0; i < states.length; i++) {
const state = states[i];
entities[state.entity_id] = state;
}
return entities;
}
const subscribeUpdates$2 = (conn, store) => conn.subscribeEvents(ev => processEvent(store, ev), "state_changed");
const entitiesColl = (conn) => getCollection(conn, "_ent", fetchEntities, subscribeUpdates$2);
const subscribeEntities = (conn, onChange) => entitiesColl(conn).subscribe(onChange);
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;
}
exports.Auth = Auth;
exports.Connection = Connection;
exports.ERR_CANNOT_CONNECT = ERR_CANNOT_CONNECT;
exports.ERR_CONNECTION_LOST = ERR_CONNECTION_LOST;
exports.ERR_HASS_HOST_REQUIRED = ERR_HASS_HOST_REQUIRED;
exports.ERR_INVALID_AUTH = ERR_INVALID_AUTH;
exports.ERR_INVALID_HTTPS_TO_HTTP = ERR_INVALID_HTTPS_TO_HTTP;
exports.callService = callService$1;
exports.createCollection = createCollection;
exports.createConnection = createConnection;
exports.entitiesColl = entitiesColl;
exports.genClientId = genClientId;
exports.genExpires = genExpires;
exports.getAuth = getAuth;
exports.getCollection = getCollection;
exports.getConfig = getConfig;
exports.getServices = getServices;
exports.getStates = getStates;
exports.getUser = getUser;
exports.subscribeConfig = subscribeConfig;
exports.subscribeEntities = subscribeEntities;
exports.subscribeServices = subscribeServices;
Object.defineProperty(exports, '__esModule', { value: true });
})));

@@ -1,12 +0,12 @@

import { ConnectionOptions } from "./types";
import { Connection } from "./connection";
export * from "./auth";
export * from "./collection";
export * from "./connection";
export * from "./config";
export * from "./services";
export * from "./entities";
export * from "./errors";
export * from "./types";
export * from "./commands";
import { ConnectionOptions } from "./types.js";
import { Connection } from "./connection.js";
export * from "./auth.js";
export * from "./collection.js";
export * from "./connection.js";
export * from "./config.js";
export * from "./services.js";
export * from "./entities.js";
export * from "./errors.js";
export * from "./types.js";
export * from "./commands.js";
export declare function createConnection(options?: Partial<ConnectionOptions>): Promise<Connection>;

@@ -1,2 +0,2 @@

import { Error } from "./types";
import { Error } from "./types.js";
export declare function auth(accessToken: string): {

@@ -3,0 +3,0 @@ type: string;

@@ -1,3 +0,3 @@

import { HassServices, UnsubscribeFunc } from "./types";
import { Connection } from "./connection";
import { HassServices, UnsubscribeFunc } from "./types.js";
import { Connection } from "./connection.js";
export declare const subscribeServices: (conn: Connection, onChange: (state: HassServices) => void) => UnsubscribeFunc;

@@ -1,2 +0,2 @@

import { ConnectionOptions } from "./types";
import { ConnectionOptions } from "./types.js";
export interface HaWebSocket extends WebSocket {

@@ -3,0 +3,0 @@ haVersion: string;

@@ -1,2 +0,2 @@

import { UnsubscribeFunc } from "./types";
import { UnsubscribeFunc } from "./types.js";
declare type Listener<State> = (state: State) => void;

@@ -3,0 +3,0 @@ declare type Action<State> = (state: State, ...args: any[]) => Partial<State> | Promise<Partial<State>> | null;

@@ -1,3 +0,3 @@

import { Auth } from "./auth";
import { HaWebSocket } from "./socket";
import { Auth } from "./auth.js";
import { HaWebSocket } from "./socket.js";
export declare type Error = 1 | 2 | 3 | 4;

@@ -4,0 +4,0 @@ export declare type UnsubscribeFunc = () => void;

export declare function parseQuery<T>(queryString: string): T;
{
"name": "home-assistant-js-websocket",
"amdName": "HAWS",
"type": "module",
"sideEffects": false,
"version": "4.5.0",
"version": "5.0.0",
"description": "Home Assistant websocket client",

@@ -10,3 +10,7 @@ "source": "lib/index.ts",

"main": "dist/haws.umd.js",
"module": "dist/haws.es.js",
"module": "dist/index.js",
"exports": {
"import": "dist/index.js",
"default": "dist/haws.umd.js"
},
"repository": {

@@ -17,6 +21,6 @@ "url": "https://github.com/home-assistant/home-assistant-js-websocket.git",

"scripts": {
"watch": "microbundle watch",
"build": "microbundle",
"test": "mocha",
"prepublishOnly": "rm -rf dist && microbundle && npm test"
"watch": "tsc --watch",
"build": "tsc && rollup dist/index.js --format umd --name HAWS --file dist/haws.umd.js",
"test": "tsc && mocha",
"prepublishOnly": "rm -rf dist && yarn build && npm test"
},

@@ -26,8 +30,11 @@ "author": "Paulus Schoutsen <paulus@paulusschoutsen.nl>",

"devDependencies": {
"@types/assert": "^1.4.6",
"@types/mocha": "^7.0.2",
"assert": "^2.0.0",
"husky": "^4.2.3",
"lint-staged": "^10.0.8",
"microbundle": "^0.11.0",
"mocha": "^7.1.0",
"prettier": "^1.19.1",
"reify": "^0.20.12",
"rollup": "^2.0.0",
"ts-node": "^8.6.2",

@@ -34,0 +41,0 @@ "typescript": "^3.8.3"

@@ -401,7 +401,7 @@ # :aerial_tramway: JavaScript websocket client for Home Assistant

createSocket() {
WebSocket;
// Open connection
const ws = new WebSocket("ws://localhost:8123");
// Functions to handle authentication with Home Assistant
// Implement yourself :)
// TODO: Handle authentication with Home Assistant yourself :)
return ws;

@@ -408,0 +408,0 @@ }

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc