home-assistant-js-websocket
Advanced tools
Comparing version 4.5.0 to 5.0.0
@@ -0,0 +0,0 @@ export declare type AuthData = { |
@@ -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 @@ } |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
32
1901
5
Yes
102423
11