Init Commit
This commit is contained in:
commit
60188b0574
850 files changed
+570948
No files matched your search
+194
@@ -0,0 +1,194 @@
|
||||
Apache License
|
||||
==============
|
||||
|
||||
_Version 2.0, January 2004_
|
||||
_<<http://www.apache.org/licenses/>>_
|
||||
|
||||
### Terms and Conditions for use, reproduction, and distribution
|
||||
|
||||
#### 1. Definitions
|
||||
|
||||
“License” shall mean the terms and conditions for use, reproduction, and
|
||||
distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
“Licensor” shall mean the copyright owner or entity authorized by the copyright
|
||||
owner that is granting the License.
|
||||
|
||||
“Legal Entity” shall mean the union of the acting entity and all other entities
|
||||
that control, are controlled by, or are under common control with that entity.
|
||||
For the purposes of this definition, “control” means **(i)** the power, direct or
|
||||
indirect, to cause the direction or management of such entity, whether by
|
||||
contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or **(iii)** beneficial ownership of such entity.
|
||||
|
||||
“You” (or “Your”) shall mean an individual or Legal Entity exercising
|
||||
permissions granted by this License.
|
||||
|
||||
“Source” form shall mean the preferred form for making modifications, including
|
||||
but not limited to software source code, documentation source, and configuration
|
||||
files.
|
||||
|
||||
“Object” form shall mean any form resulting from mechanical transformation or
|
||||
translation of a Source form, including but not limited to compiled object code,
|
||||
generated documentation, and conversions to other media types.
|
||||
|
||||
“Work” shall mean the work of authorship, whether in Source or Object form, made
|
||||
available under the License, as indicated by a copyright notice that is included
|
||||
in or attached to the work (an example is provided in the Appendix below).
|
||||
|
||||
“Derivative Works” shall mean any work, whether in Source or Object form, that
|
||||
is based on (or derived from) the Work and for which the editorial revisions,
|
||||
annotations, elaborations, or other modifications represent, as a whole, an
|
||||
original work of authorship. For the purposes of this License, Derivative Works
|
||||
shall not include works that remain separable from, or merely link (or bind by
|
||||
name) to the interfaces of, the Work and Derivative Works thereof.
|
||||
|
||||
“Contribution” shall mean any work of authorship, including the original version
|
||||
of the Work and any modifications or additions to that Work or Derivative Works
|
||||
thereof, that is intentionally submitted to Licensor for inclusion in the Work
|
||||
by the copyright owner or by an individual or Legal Entity authorized to submit
|
||||
on behalf of the copyright owner. For the purposes of this definition,
|
||||
“submitted” means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems, and
|
||||
issue tracking systems that are managed by, or on behalf of, the Licensor for
|
||||
the purpose of discussing and improving the Work, but excluding communication
|
||||
that is conspicuously marked or otherwise designated in writing by the copyright
|
||||
owner as “Not a Contribution.”
|
||||
|
||||
“Contributor” shall mean Licensor and any individual or Legal Entity on behalf
|
||||
of whom a Contribution has been received by Licensor and subsequently
|
||||
incorporated within the Work.
|
||||
|
||||
#### 2. Grant of Copyright License
|
||||
|
||||
Subject to the terms and conditions of this License, each Contributor hereby
|
||||
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
|
||||
irrevocable copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the Work and such
|
||||
Derivative Works in Source or Object form.
|
||||
|
||||
#### 3. Grant of Patent License
|
||||
|
||||
Subject to the terms and conditions of this License, each Contributor hereby
|
||||
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
|
||||
irrevocable (except as stated in this section) patent license to make, have
|
||||
made, use, offer to sell, sell, import, and otherwise transfer the Work, where
|
||||
such license applies only to those patent claims licensable by such Contributor
|
||||
that are necessarily infringed by their Contribution(s) alone or by combination
|
||||
of their Contribution(s) with the Work to which such Contribution(s) was
|
||||
submitted. If You institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work or a
|
||||
Contribution incorporated within the Work constitutes direct or contributory
|
||||
patent infringement, then any patent licenses granted to You under this License
|
||||
for that Work shall terminate as of the date such litigation is filed.
|
||||
|
||||
#### 4. Redistribution
|
||||
|
||||
You may reproduce and distribute copies of the Work or Derivative Works thereof
|
||||
in any medium, with or without modifications, and in Source or Object form,
|
||||
provided that You meet the following conditions:
|
||||
|
||||
* **(a)** You must give any other recipients of the Work or Derivative Works a copy of
|
||||
this License; and
|
||||
* **(b)** You must cause any modified files to carry prominent notices stating that You
|
||||
changed the files; and
|
||||
* **(c)** You must retain, in the Source form of any Derivative Works that You distribute,
|
||||
all copyright, patent, trademark, and attribution notices from the Source form
|
||||
of the Work, excluding those notices that do not pertain to any part of the
|
||||
Derivative Works; and
|
||||
* **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any
|
||||
Derivative Works that You distribute must include a readable copy of the
|
||||
attribution notices contained within such NOTICE file, excluding those notices
|
||||
that do not pertain to any part of the Derivative Works, in at least one of the
|
||||
following places: within a NOTICE text file distributed as part of the
|
||||
Derivative Works; within the Source form or documentation, if provided along
|
||||
with the Derivative Works; or, within a display generated by the Derivative
|
||||
Works, if and wherever such third-party notices normally appear. The contents of
|
||||
the NOTICE file are for informational purposes only and do not modify the
|
||||
License. You may add Your own attribution notices within Derivative Works that
|
||||
You distribute, alongside or as an addendum to the NOTICE text from the Work,
|
||||
provided that such additional attribution notices cannot be construed as
|
||||
modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and may provide
|
||||
additional or different license terms and conditions for use, reproduction, or
|
||||
distribution of Your modifications, or for any such Derivative Works as a whole,
|
||||
provided Your use, reproduction, and distribution of the Work otherwise complies
|
||||
with the conditions stated in this License.
|
||||
|
||||
#### 5. Submission of Contributions
|
||||
|
||||
Unless You explicitly state otherwise, any Contribution intentionally submitted
|
||||
for inclusion in the Work by You to the Licensor shall be under the terms and
|
||||
conditions of this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify the terms of
|
||||
any separate license agreement you may have executed with Licensor regarding
|
||||
such Contributions.
|
||||
|
||||
#### 6. Trademarks
|
||||
|
||||
This License does not grant permission to use the trade names, trademarks,
|
||||
service marks, or product names of the Licensor, except as required for
|
||||
reasonable and customary use in describing the origin of the Work and
|
||||
reproducing the content of the NOTICE file.
|
||||
|
||||
#### 7. Disclaimer of Warranty
|
||||
|
||||
Unless required by applicable law or agreed to in writing, Licensor provides the
|
||||
Work (and each Contributor provides its Contributions) on an “AS IS” BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
|
||||
including, without limitation, any warranties or conditions of TITLE,
|
||||
NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
|
||||
solely responsible for determining the appropriateness of using or
|
||||
redistributing the Work and assume any risks associated with Your exercise of
|
||||
permissions under this License.
|
||||
|
||||
#### 8. Limitation of Liability
|
||||
|
||||
In no event and under no legal theory, whether in tort (including negligence),
|
||||
contract, or otherwise, unless required by applicable law (such as deliberate
|
||||
and grossly negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special, incidental,
|
||||
or consequential damages of any character arising as a result of this License or
|
||||
out of the use or inability to use the Work (including but not limited to
|
||||
damages for loss of goodwill, work stoppage, computer failure or malfunction, or
|
||||
any and all other commercial damages or losses), even if such Contributor has
|
||||
been advised of the possibility of such damages.
|
||||
|
||||
#### 9. Accepting Warranty or Additional Liability
|
||||
|
||||
While redistributing the Work or Derivative Works thereof, You may choose to
|
||||
offer, and charge a fee for, acceptance of support, warranty, indemnity, or
|
||||
other liability obligations and/or rights consistent with this License. However,
|
||||
in accepting such obligations, You may act only on Your own behalf and on Your
|
||||
sole responsibility, not on behalf of any other Contributor, and only if You
|
||||
agree to indemnify, defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason of your
|
||||
accepting any such warranty or additional liability.
|
||||
|
||||
_END OF TERMS AND CONDITIONS_
|
||||
|
||||
### APPENDIX: How to apply the Apache License to your work
|
||||
|
||||
To apply the Apache License to your work, attach the following boilerplate
|
||||
notice, with the fields enclosed by brackets `[]` replaced with your own
|
||||
identifying information. (Don't include the brackets!) The text should be
|
||||
enclosed in the appropriate comment syntax for the file format. We also
|
||||
recommend that a file or class name and description of purpose be included on
|
||||
the same “printed page” as the copyright notice for easier identification within
|
||||
third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
+472
@@ -0,0 +1,472 @@
|
||||
# :aerial_tramway: JavaScript websocket client for Home Assistant
|
||||
|
||||
This is a websocket client written in JavaScript that allows retrieving authentication tokens and communicate with the Home Assistant websocket API. It can be used to integrate Home Assistant into your apps. It has 0 dependencies.
|
||||
|
||||
## Trying it out
|
||||
|
||||
Check [the demo](https://home-assistant.github.io/home-assistant-js-websocket/) ([source](https://github.com/home-assistant/home-assistant-js-websocket/blob/master/index.html)).
|
||||
|
||||
Clone this repository, then go to home-assistant-js-websocket folder and run the following commands:
|
||||
|
||||
```bash
|
||||
yarn install
|
||||
yarn build
|
||||
npx http-server -o
|
||||
# A browser will open, navigate to example.html
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
# With npm
|
||||
npm i home-assistant-js-websocket
|
||||
|
||||
# With yarn
|
||||
yarn add home-assistant-js-websocket
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
To initialize a connection, you need an authentication token for the instance that you want to connect to. This library implements the necessary steps to guide the user to authenticate your website with their Home Assistant instance and give you a token. All you need from the user is the url of their instance.
|
||||
|
||||
```js
|
||||
// Example connect code
|
||||
import {
|
||||
getAuth,
|
||||
createConnection,
|
||||
subscribeEntities,
|
||||
ERR_HASS_HOST_REQUIRED,
|
||||
} from "home-assistant-js-websocket";
|
||||
|
||||
async function connect() {
|
||||
let auth;
|
||||
try {
|
||||
// Try to pick up authentication after user logs in
|
||||
auth = await getAuth();
|
||||
} catch (err) {
|
||||
if (err === ERR_HASS_HOST_REQUIRED) {
|
||||
const hassUrl = prompt(
|
||||
"What host to connect to?",
|
||||
"http://localhost:8123",
|
||||
);
|
||||
// Redirect user to log in on their instance
|
||||
auth = await getAuth({ hassUrl });
|
||||
} else {
|
||||
alert(`Unknown error: ${err}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
const connection = await createConnection({ auth });
|
||||
subscribeEntities(connection, (ent) => console.log(ent));
|
||||
}
|
||||
|
||||
connect();
|
||||
```
|
||||
|
||||
### `getAuth()`
|
||||
|
||||
Use this method to get authentication from a server via OAuth2. This method will handle redirecting to an instance and fetching the token after the user successful logs in.
|
||||
|
||||
You can pass options using the syntax:
|
||||
|
||||
```js
|
||||
getAuth({ hassUrl: "http://localhost:8123" });
|
||||
```
|
||||
|
||||
| Option | Description |
|
||||
| ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| hassUrl | The url where the Home Assistant instance can be reached. This option is needed so we know where to redirect the user for authentication. Once redirected back, it is not needed to pass this option in. |
|
||||
| clientId | Client ID to use. Client IDs for Home Assistant is the url of your application. Defaults to domain of current page. Pass `null` if you are making requests on behalf of a system user. |
|
||||
| redirectUrl | The url to redirect back to when the user has logged in. Defaults to current page. |
|
||||
| saveTokens | Function to store an object containing the token information. |
|
||||
| loadTokens | Function that returns a promise that resolves to previously stored token information object or undefined if no info available. |
|
||||
| authCode | If you have an auth code received via other means, you can pass it in and it will be used to fetch tokens instead of going through the OAuth2 flow. |
|
||||
| limitHassInstance | If set to true, allow only authentication credentials for the passed in `hassUrl` and `clientId`. Defaults to false. |
|
||||
|
||||
In certain instances `getAuth` will raise an error. These errors can be imported from the package:
|
||||
|
||||
```js
|
||||
// When bundling your application
|
||||
import {
|
||||
ERR_HASS_HOST_REQUIRED,
|
||||
ERR_INVALID_AUTH,
|
||||
} from "home-assistant-js-websocket";
|
||||
|
||||
// When using the UMD build
|
||||
HAWS.ERR_HASS_HOST_REQUIRED;
|
||||
```
|
||||
|
||||
| Error | Description |
|
||||
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `ERR_HASS_HOST_REQUIRED` | You need to pass in `hassUrl` to `getAuth` to continue getting auth. This option is not needed when the user is redirected back after successfully logging in. |
|
||||
| `ERR_INVALID_AUTH` | This error will be raised if the url contains an authorization code that is no longer valid. |
|
||||
| `ERR_INVALID_HTTPS_TO_HTTP` | This error is raised if your code is being run from a secure context (hosted via https) and you're trying to fetch tokens from a Home Assistant instance via a non secure context (http). This is called [mixed active content](https://developer.mozilla.org/en-US/docs/Web/Security/Mixed_content#Mixed_active_content) and the browser forbids this. |
|
||||
| `ERR_INVALID_AUTH_CALLBACK` | This error is raised if only credentials for the specified Home Assistant instance are allowed and the client ID or hassURL in the auth callback state do not match the expected ones. |
|
||||
| Other errors | Unknown error! |
|
||||
|
||||
### `createConnection()`
|
||||
|
||||
You need to either provide `auth` or `createSocket` as options to createConnection:
|
||||
|
||||
```js
|
||||
createConnection({ auth });
|
||||
```
|
||||
|
||||
| Option | Description |
|
||||
| ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| auth | Auth object to use to create a connection. |
|
||||
| createSocket | Override the createSocket method with your own. `(options) => Promise<WebSocket>`. Needs to return a connection that is already authenticated. |
|
||||
| setupRetry | Number of times to retry initial connection when it fails. Set to -1 for infinite retries. Default is 0 (no retries) |
|
||||
|
||||
Currently the following error codes can be raised by createConnection:
|
||||
|
||||
| Error | Description |
|
||||
| ------------------ | --------------------------------------------------------- |
|
||||
| ERR_CANNOT_CONNECT | If the client was unable to connect to the websocket API. |
|
||||
| ERR_INVALID_AUTH | If the supplied authentication was invalid. |
|
||||
|
||||
You can import them into your code as follows:
|
||||
|
||||
```javascript
|
||||
import {
|
||||
ERR_CANNOT_CONNECT,
|
||||
ERR_INVALID_AUTH,
|
||||
} from "home-assistant-js-websocket";
|
||||
```
|
||||
|
||||
### Automatic reconnecting
|
||||
|
||||
The connection object will automatically try to reconnect to the server when the connection gets lost. On reconnect, it will automatically resubscribe the event listeners.
|
||||
|
||||
#### Suspend reconnection
|
||||
|
||||
If you don't want to automatically try to reconnect to the server when the connection is lost, you can pass a promise to wait for. The connection will try to reconnect after the promise is resolved.
|
||||
|
||||
```javascript
|
||||
connection.suspendReconnectUntil(
|
||||
new Promise((resolve) => {
|
||||
// When you want to try to reconnect again, resolve the promise.
|
||||
resolve();
|
||||
}),
|
||||
);
|
||||
```
|
||||
|
||||
When the suspend promise resolves until the connection is re-established, all messages being send will be delayed until the connection is established. If the first reconnect fails, the queued messages will be rejected.
|
||||
|
||||
#### Suspend connection
|
||||
|
||||
You can also actively close the connection and wait for a promise to resolve to reconnect again. This promise can be passed either with `suspendReconnectUntil` or with the `suspend` command itself.
|
||||
|
||||
If you don't provide a promise with either of these functions, an error will be thrown.
|
||||
|
||||
```javascript
|
||||
connection.suspendReconnectUntil(
|
||||
new Promise((resolve) => {
|
||||
// When you want to try to reconnect again, resolve the promise.
|
||||
resolve();
|
||||
}),
|
||||
);
|
||||
connection.suspend();
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```javascript
|
||||
connection.suspend(
|
||||
new Promise((resolve) => {
|
||||
// When you want to try to reconnect again, resolve the promise.
|
||||
resolve();
|
||||
}),
|
||||
);
|
||||
```
|
||||
|
||||
#### Close connection
|
||||
|
||||
You can also close the connection, without any reconnect, with `connection.close()`.
|
||||
|
||||
#### Events
|
||||
|
||||
The `Connection` object implements three events related to the reconnecting logic.
|
||||
|
||||
| Event | Data | Description |
|
||||
| --------------- | ---------- | -------------------------------------------------------------------------------------------------------- |
|
||||
| ready | - | Fired when authentication is successful and the connection is ready to take commands. |
|
||||
| disconnected | - | Fired when the connection is lost. |
|
||||
| reconnect-error | Error code | Fired when we encounter a fatal error when trying to reconnect. Currently limited to `ERR_INVALID_AUTH`. |
|
||||
|
||||
You can attach and remove listeners as follows:
|
||||
|
||||
```javascript
|
||||
function eventHandler(connection, data) {
|
||||
console.log("Connection has been established again");
|
||||
}
|
||||
|
||||
conn.addEventListener("ready", eventHandler);
|
||||
conn.removeEventListener("ready", eventHandler);
|
||||
```
|
||||
|
||||
### Entities
|
||||
|
||||
You can subscribe to the entities of Home Assistant. Your callback will be called when the entities are first loaded and on every change to the state of any of the entities after that. The callback will be called with a single object that contains the entities keyed by entity_id.
|
||||
|
||||
The function `subscribeEntities` will return an unsubscribe function.
|
||||
|
||||
```javascript
|
||||
import { subscribeEntities } from "home-assistant-js-websocket";
|
||||
|
||||
// conn is the connection from earlier.
|
||||
subscribeEntities(conn, (entities) => console.log("New entities!", entities));
|
||||
```
|
||||
|
||||
You can also import the collection:
|
||||
|
||||
```javascript
|
||||
import { entitiesColl } from "home-assistant-js-websocket";
|
||||
|
||||
// conn is the connection from earlier.
|
||||
const coll = entitiesColl(conn);
|
||||
console.log(coll.state);
|
||||
await coll.refresh();
|
||||
coll.subscribe((entities) => console.log(entities));
|
||||
```
|
||||
|
||||
### Config
|
||||
|
||||
You can subscribe to the config of Home Assistant. Config can change when either a component gets loaded.
|
||||
|
||||
The function `subscribeConfig` will return an unsubscribe function.
|
||||
|
||||
```javascript
|
||||
import { subscribeConfig } from "home-assistant-js-websocket";
|
||||
|
||||
// conn is the connection from earlier.
|
||||
subscribeConfig(conn, (config) => console.log("New config!", config));
|
||||
```
|
||||
|
||||
You can also import the collection:
|
||||
|
||||
```javascript
|
||||
import { configColl } from "home-assistant-js-websocket";
|
||||
|
||||
// conn is the connection from earlier.
|
||||
const coll = configColl(connection);
|
||||
console.log(coll.state);
|
||||
await coll.refresh();
|
||||
coll.subscribe((config) => console.log(config));
|
||||
```
|
||||
|
||||
### Services
|
||||
|
||||
You can subscribe to the available services of Home Assistant. Services can change when a new service gets registered or removed.
|
||||
|
||||
The function `subscribeServices` will return an unsubscribe function.
|
||||
|
||||
```javascript
|
||||
import { subscribeServices } from "home-assistant-js-websocket";
|
||||
|
||||
// conn is the connection from earlier.
|
||||
subscribeServices(conn, (services) => console.log("New services!", services));
|
||||
```
|
||||
|
||||
You can also import the collection:
|
||||
|
||||
```javascript
|
||||
import { servicesColl } from "home-assistant-js-websocket";
|
||||
|
||||
// conn is the connection from earlier.
|
||||
const coll = servicesColl(connection);
|
||||
console.log(coll.state);
|
||||
await coll.refresh();
|
||||
coll.subscribe((services) => console.log(services));
|
||||
```
|
||||
|
||||
### Collections
|
||||
|
||||
Besides entities, config and services you might want to create your own collections. A collection has the following features:
|
||||
|
||||
- Fetch a full data set on initial creation and on reconnect
|
||||
- Subscribe to events to keep collection up to date
|
||||
- Share subscription between multiple listeners
|
||||
- Unsubscribe when no listeners
|
||||
- Manually trigger a refresh
|
||||
|
||||
```typescript
|
||||
// Will only initialize one collection per connection.
|
||||
getCollection<State>(
|
||||
conn: Connection,
|
||||
key: string,
|
||||
fetchCollection: (conn: Connection) => Promise<State>,
|
||||
subscribeUpdates: (
|
||||
conn: Connection,
|
||||
store: Store<State>
|
||||
) => Promise<UnsubscribeFunc>,
|
||||
): Collection<State>
|
||||
|
||||
// Returns object with following type
|
||||
class Collection<State> {
|
||||
state: State;
|
||||
async refresh(): Promise<void>;
|
||||
subscribe(subscriber: (state: State) => void): UnsubscribeFunc;
|
||||
}
|
||||
```
|
||||
|
||||
- `conn` is the connection to subscribe to.
|
||||
- `key` a unique key for the collection
|
||||
- `fetchCollection` needs to return a Promsise that resolves to the full state
|
||||
- `subscribeUpdates` needs to subscribe to the updates and update the store. Returns a promise that resolves to an unsubscribe function.
|
||||
|
||||
#### Collection Example
|
||||
|
||||
```javascript
|
||||
import { getCollection } from "home-assistant-js-websocket";
|
||||
|
||||
function panelRegistered(state, event) {
|
||||
// Returning null means no change.
|
||||
if (state === undefined) return null;
|
||||
|
||||
// This will be merged with the existing state.
|
||||
return {
|
||||
panels: state.panels.concat(event.data.panel),
|
||||
};
|
||||
}
|
||||
|
||||
const fetchPanels = (conn) => conn.sendMessagePromise({ type: "get_panels" });
|
||||
const subscribeUpdates = (conn, store) =>
|
||||
conn.subscribeEvents(store.action(panelRegistered), "panel_registered");
|
||||
|
||||
const panelsColl = getCollection(conn, "_pnl", fetchPanels, subscribeUpdates);
|
||||
|
||||
// Now use collection
|
||||
console.log(panelsColl.state);
|
||||
await panelsColl.refresh();
|
||||
panelsColl.subscribe((panels) => console.log("New panels!", panels));
|
||||
```
|
||||
|
||||
Collections are useful to define if data is needed for initial data load. You can create a collection and have code on your page call it before you start rendering the UI. By the time UI is loaded, the data will be available to use.
|
||||
|
||||
## Connection API Reference
|
||||
|
||||
A connection object is obtained by calling [`createConnection()`](#createconnection).
|
||||
|
||||
##### `conn.haVersion`
|
||||
|
||||
String containing the current version of Home Assistant. Examples:
|
||||
|
||||
- `0.107.0`
|
||||
- `0.107.0b1`
|
||||
- `0.107.0.dev0`
|
||||
- `2021.3.0`
|
||||
|
||||
##### `conn.subscribeEvents(eventCallback[, eventType])`
|
||||
|
||||
Subscribe to all or specific events on the Home Assistant bus. Calls `eventCallback` for each event that gets received.
|
||||
|
||||
Returns a promise that will resolve to a function that will cancel the subscription once called.
|
||||
|
||||
Subscription will be automatically re-established after a reconnect.
|
||||
|
||||
Uses `conn.subscribeMessage` under the hood.
|
||||
|
||||
##### `conn.addEventListener(eventType, listener)`
|
||||
|
||||
Listen for events on the connection. [See docs.](#automatic-reconnecting)
|
||||
|
||||
##### `conn.sendMessagePromise(message)`
|
||||
|
||||
Send a message to the server. Returns a promise that resolves or rejects based on the result of the server. Special case rejection is `ERR_CONNECTION_LOST` if the connection is lost while the command is in progress.
|
||||
|
||||
##### `conn.subscribeMessage(callback, subscribeMessage[, options])`
|
||||
|
||||
Call an endpoint in Home Assistant that creates a subscription. Calls `callback` for each item that gets received.
|
||||
|
||||
Returns a promise that will resolve to a function that will cancel the subscription once called.
|
||||
|
||||
Subscription will be automatically re-established after a reconnect unless `options.resubscribe` is false.
|
||||
|
||||
| Option | Description |
|
||||
| ----------- | ----------------------------------------------- |
|
||||
| resubscribe | Re-established a subscription after a reconnect |
|
||||
|
||||
## Auth API Reference
|
||||
|
||||
An instance of Auth is returned from the `getAuth` method. It has the following properties:
|
||||
|
||||
- `wsUrl`: the websocket url of the instance
|
||||
- `accessToken`: the access token
|
||||
- `expired`: boolean that indicates if the access token has expired
|
||||
|
||||
##### `auth.refreshAccessToken()`
|
||||
|
||||
Fetches a new access token from the server.
|
||||
|
||||
##### `auth.revoke()`
|
||||
|
||||
Makes a request to the server to revoke the refresh and all related access token. Returns a promise that resolves when the request is finished.
|
||||
|
||||
**Note:** If you support storing and retrieving tokens, the returned auth object might load tokens from your cache that are no longer valid. If this happens, the promise returned by `createConnection` will reject with `ERR_INVALID_AUTH`. If that happens, clear your tokens with `storeTokens(null`) and call `getAuth` again. This will pick up the auth flow without relying on stored tokens.
|
||||
|
||||
## Error Reference
|
||||
|
||||
| Error constant | Error number |
|
||||
| ------------------------- | ------------ |
|
||||
| ERR_CANNOT_CONNECT | 1 |
|
||||
| ERR_INVALID_AUTH | 2 |
|
||||
| ERR_CONNECTION_LOST | 3 |
|
||||
| ERR_HASS_HOST_REQUIRED | 4 |
|
||||
| ERR_INVALID_HTTPS_TO_HTTP | 5 |
|
||||
|
||||
## Other methods
|
||||
|
||||
The library also contains a few helper method that you can use to ineract with the API.
|
||||
|
||||
- `getUser(connection) -> Promise<HassUser>`
|
||||
- `callService(connection, domain, service, serviceData?, target?, returnResponse?) -> Promise` (Support for `target` was added in Home Assistant Core 2021.3)
|
||||
|
||||
The following are also available, but it's recommended that you use the subscribe methods documented above.
|
||||
|
||||
- `getStates(connection) -> Promise<HassEntity[]>`
|
||||
- `getServices(connection) -> Promise<HassEntity[]>`
|
||||
- `getConfig(connection) -> Promise<HassEntity[]>`
|
||||
|
||||
## Using this with long-lived access tokens
|
||||
|
||||
If you are in a browser, you should prefer to use the `getAuth()` flow. This will use the more secure refresh/access token pair. If that is not possible, you can ask the user to create a long-lived access token.
|
||||
|
||||
You will need to create your own auth object if you want to use this library with a long-lived access token.
|
||||
|
||||
```js
|
||||
import {
|
||||
Auth,
|
||||
createConnection,
|
||||
subscribeEntities,
|
||||
createLongLivedTokenAuth,
|
||||
} from "home-assistant-js-websocket";
|
||||
|
||||
(async () => {
|
||||
const auth = createLongLivedTokenAuth(
|
||||
"http://localhost:8123",
|
||||
"YOUR ACCESS TOKEN",
|
||||
);
|
||||
|
||||
const connection = await createConnection({ auth });
|
||||
subscribeEntities(connection, (entities) => console.log(entities));
|
||||
})();
|
||||
```
|
||||
|
||||
## Using this in NodeJS
|
||||
|
||||
NodeJS does not have a WebSocket client built-in, but there are some good ones on NPM. We recommend ws. The easiest way to enable WebSocket is to polyfill it into the global namespace.
|
||||
Look at https://github.com/keesschollaart81/vscode-home-assistant/blob/master/src/language-service/src/home-assistant/socket.ts as an example using ws.
|
||||
|
||||
If using TypeScript, you will need to install `@types/ws` as well.
|
||||
|
||||
```js
|
||||
globalThis.WebSocket = require("ws");
|
||||
```
|
||||
|
||||
or in TypeScript:
|
||||
|
||||
```ts
|
||||
const wnd = globalThis;
|
||||
wnd.WebSocket = require("ws");
|
||||
```
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
export type AuthData = {
|
||||
hassUrl: string;
|
||||
clientId: string | null;
|
||||
expires: number;
|
||||
refresh_token: string;
|
||||
access_token: string;
|
||||
expires_in: number;
|
||||
};
|
||||
export type SaveTokensFunc = (data: AuthData | null) => void;
|
||||
export type LoadTokensFunc = () => Promise<AuthData | null | undefined>;
|
||||
export type getAuthOptions = {
|
||||
hassUrl?: string;
|
||||
clientId?: string | null;
|
||||
redirectUrl?: string;
|
||||
authCode?: string;
|
||||
saveTokens?: SaveTokensFunc;
|
||||
loadTokens?: LoadTokensFunc;
|
||||
limitHassInstance?: boolean;
|
||||
};
|
||||
export declare const genClientId: () => string;
|
||||
export declare const genExpires: (expires_in: number) => number;
|
||||
export declare class Auth {
|
||||
private _saveTokens?;
|
||||
data: AuthData;
|
||||
constructor(data: AuthData, saveTokens?: SaveTokensFunc);
|
||||
get wsUrl(): string;
|
||||
get accessToken(): string;
|
||||
get expired(): boolean;
|
||||
/**
|
||||
* Refresh the access token.
|
||||
*/
|
||||
refreshAccessToken(): Promise<void>;
|
||||
/**
|
||||
* Revoke the refresh & access tokens.
|
||||
*/
|
||||
revoke(): Promise<void>;
|
||||
}
|
||||
export declare function createLongLivedTokenAuth(hassUrl: string, access_token: string): Auth;
|
||||
export declare function getAuth(options?: getAuthOptions): Promise<Auth>;
|
||||
+188
@@ -0,0 +1,188 @@
|
||||
import { parseQuery } from "./util.js";
|
||||
import { ERR_HASS_HOST_REQUIRED, ERR_INVALID_AUTH, ERR_INVALID_AUTH_CALLBACK, ERR_INVALID_HTTPS_TO_HTTP, } from "./errors.js";
|
||||
export const genClientId = () => `${location.protocol}//${location.host}/`;
|
||||
export 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) => {
|
||||
// @ts-ignore
|
||||
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));
|
||||
}
|
||||
export 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() {
|
||||
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,
|
||||
});
|
||||
// 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() {
|
||||
if (!this.data.refresh_token)
|
||||
throw new Error("No refresh_token to revoke");
|
||||
const formData = new FormData();
|
||||
formData.append("token", this.data.refresh_token);
|
||||
// There is no error checking, as revoke will always return 200
|
||||
await fetch(`${this.data.hassUrl}/auth/revoke`, {
|
||||
method: "POST",
|
||||
credentials: "same-origin",
|
||||
body: formData,
|
||||
});
|
||||
if (this._saveTokens) {
|
||||
this._saveTokens(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
export function createLongLivedTokenAuth(hassUrl, access_token) {
|
||||
return new Auth({
|
||||
hassUrl,
|
||||
clientId: null,
|
||||
expires: Date.now() + 1e11,
|
||||
refresh_token: "",
|
||||
access_token,
|
||||
expires_in: 1e11,
|
||||
});
|
||||
}
|
||||
export 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();
|
||||
const limitHassInstance = options.limitHassInstance === true;
|
||||
// Use auth code if it was passed in
|
||||
if (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);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Check for stored tokens
|
||||
if (!data && options.loadTokens) {
|
||||
data = await options.loadTokens();
|
||||
}
|
||||
// If the token is for another url, ignore it
|
||||
if (data && (hassUrl === undefined || data.hassUrl === hassUrl)) {
|
||||
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(() => { });
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
import { Store } from "./store.js";
|
||||
import { Connection } from "./connection.js";
|
||||
import { UnsubscribeFunc } from "./types.js";
|
||||
export type Collection<State> = {
|
||||
state: State;
|
||||
refresh(): Promise<void>;
|
||||
subscribe(subscriber: (state: State) => void): UnsubscribeFunc;
|
||||
};
|
||||
/**
|
||||
*
|
||||
* @param conn connection
|
||||
* @param key the key to store it on the connection. Must be unique for each collection.
|
||||
* @param fetchCollection fetch the current state. If undefined assumes subscribeUpdates receives current state
|
||||
* @param subscribeUpdates subscribe to updates on the current state
|
||||
* @returns
|
||||
*/
|
||||
export declare const getCollection: <State>(conn: Connection, key: string, fetchCollection: ((conn: Connection) => Promise<State>) | undefined, subscribeUpdates?: (conn: Connection, store: Store<State>) => Promise<UnsubscribeFunc>, options?: {
|
||||
unsubGrace: boolean;
|
||||
}) => Collection<State>;
|
||||
export declare const createCollection: <State>(key: string, fetchCollection: (conn: Connection) => Promise<State>, subscribeUpdates: ((conn: Connection, store: Store<State>) => Promise<UnsubscribeFunc>) | undefined, conn: Connection, onChange: (state: State) => void) => UnsubscribeFunc;
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
import { createStore } from "./store.js";
|
||||
// Time to wait to unsubscribe from updates after last subscriber unsubscribes
|
||||
const UNSUB_GRACE_PERIOD = 5000; // 5 seconds
|
||||
const DEBUG = false;
|
||||
/**
|
||||
*
|
||||
* @param conn connection
|
||||
* @param key the key to store it on the connection. Must be unique for each collection.
|
||||
* @param fetchCollection fetch the current state. If undefined assumes subscribeUpdates receives current state
|
||||
* @param subscribeUpdates subscribe to updates on the current state
|
||||
* @returns
|
||||
*/
|
||||
export const getCollection = (conn, key, fetchCollection, subscribeUpdates, options = { unsubGrace: true }) => {
|
||||
// @ts-ignore
|
||||
if (conn[key]) {
|
||||
// @ts-ignore
|
||||
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) => {
|
||||
// Swallow errors if socket is connecting, closing or closed.
|
||||
// We will automatically call refresh again when we re-establish the connection.
|
||||
if (conn.connected) {
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
const setupUpdateSubscription = () => {
|
||||
if (unsubTimer !== undefined) {
|
||||
if (DEBUG) {
|
||||
console.log(`Prevented unsubscribe for ${key}`);
|
||||
}
|
||||
clearTimeout(unsubTimer);
|
||||
unsubTimer = undefined;
|
||||
return;
|
||||
}
|
||||
if (DEBUG) {
|
||||
console.log(`Subscribing to ${key}`);
|
||||
}
|
||||
if (subscribeUpdates) {
|
||||
unsubProm = subscribeUpdates(conn, store);
|
||||
}
|
||||
if (fetchCollection) {
|
||||
// Fetch when connection re-established.
|
||||
conn.addEventListener("ready", refreshSwallow);
|
||||
refreshSwallow();
|
||||
}
|
||||
conn.addEventListener("disconnected", handleDisconnect);
|
||||
};
|
||||
const teardownUpdateSubscription = () => {
|
||||
if (DEBUG) {
|
||||
console.log(`Unsubscribing from ${key}`);
|
||||
}
|
||||
unsubTimer = undefined;
|
||||
// Unsubscribe from changes
|
||||
if (unsubProm)
|
||||
unsubProm.then((unsub) => {
|
||||
unsub();
|
||||
});
|
||||
store.clearState();
|
||||
conn.removeEventListener("ready", refresh);
|
||||
conn.removeEventListener("disconnected", handleDisconnect);
|
||||
};
|
||||
const scheduleTeardownUpdateSubscription = () => {
|
||||
if (DEBUG) {
|
||||
console.log(`Scheduling unsubscribing from ${key}`);
|
||||
}
|
||||
unsubTimer = setTimeout(teardownUpdateSubscription, UNSUB_GRACE_PERIOD);
|
||||
};
|
||||
const handleDisconnect = () => {
|
||||
// If we're going to unsubscribe and then lose connection,
|
||||
// just unsubscribe immediately.
|
||||
if (unsubTimer) {
|
||||
clearTimeout(unsubTimer);
|
||||
teardownUpdateSubscription();
|
||||
}
|
||||
};
|
||||
// @ts-ignore
|
||||
conn[key] = {
|
||||
get state() {
|
||||
return store.state;
|
||||
},
|
||||
refresh,
|
||||
subscribe(subscriber) {
|
||||
active++;
|
||||
if (DEBUG) {
|
||||
console.log(`New subscriber for ${key}. Active subscribers: ${active}`);
|
||||
}
|
||||
// If this was the first subscriber, attach collection
|
||||
if (active === 1) {
|
||||
setupUpdateSubscription();
|
||||
}
|
||||
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 (DEBUG) {
|
||||
console.log(`Unsubscribe for ${key}. Active subscribers: ${active}`);
|
||||
}
|
||||
if (!active) {
|
||||
options.unsubGrace
|
||||
? scheduleTeardownUpdateSubscription()
|
||||
: teardownUpdateSubscription();
|
||||
}
|
||||
};
|
||||
},
|
||||
};
|
||||
// @ts-ignore
|
||||
return conn[key];
|
||||
};
|
||||
// Legacy name. It gets a collection and subscribes.
|
||||
export const createCollection = (key, fetchCollection, subscribeUpdates, conn, onChange) => getCollection(conn, key, fetchCollection, subscribeUpdates).subscribe(onChange);
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
import { Connection } from "./connection.js";
|
||||
import { HassEntity, HassServices, HassConfig, HassUser, HassServiceTarget } from "./types.js";
|
||||
export declare const getStates: (connection: Connection) => Promise<HassEntity[]>;
|
||||
export declare const getServices: (connection: Connection) => Promise<HassServices>;
|
||||
export declare const getConfig: (connection: Connection) => Promise<HassConfig>;
|
||||
export declare const getUser: (connection: Connection) => Promise<HassUser>;
|
||||
export declare const callService: (connection: Connection, domain: string, service: string, serviceData?: object, target?: HassServiceTarget, returnResponse?: boolean) => Promise<unknown>;
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
import * as messages from "./messages.js";
|
||||
export const getStates = (connection) => connection.sendMessagePromise(messages.states());
|
||||
export const getServices = (connection) => connection.sendMessagePromise(messages.services());
|
||||
export const getConfig = (connection) => connection.sendMessagePromise(messages.config());
|
||||
export const getUser = (connection) => connection.sendMessagePromise(messages.user());
|
||||
export const callService = (connection, domain, service, serviceData, target, returnResponse) => connection.sendMessagePromise(messages.callService(domain, service, serviceData, target, returnResponse));
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
import { HassConfig, UnsubscribeFunc } from "./types.js";
|
||||
import { Connection } from "./connection.js";
|
||||
export declare const configColl: (conn: Connection) => import("./collection.js").Collection<HassConfig>;
|
||||
export declare const subscribeConfig: (conn: Connection, onChange: (state: HassConfig) => void) => UnsubscribeFunc;
|
||||
export declare const STATE_NOT_RUNNING = "NOT_RUNNING";
|
||||
export declare const STATE_STARTING = "STARTING";
|
||||
export declare const STATE_RUNNING = "RUNNING";
|
||||
export declare const STATE_STOPPING = "STOPPING";
|
||||
export declare const STATE_FINAL_WRITE = "FINAL_WRITE";
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
import { getCollection } from "./collection.js";
|
||||
import { getConfig } from "./commands.js";
|
||||
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()));
|
||||
export const configColl = (conn) => getCollection(conn, "_cnf", fetchConfig, subscribeUpdates);
|
||||
export const subscribeConfig = (conn, onChange) => configColl(conn).subscribe(onChange);
|
||||
export const STATE_NOT_RUNNING = "NOT_RUNNING";
|
||||
export const STATE_STARTING = "STARTING";
|
||||
export const STATE_RUNNING = "RUNNING";
|
||||
export const STATE_STOPPING = "STOPPING";
|
||||
export const STATE_FINAL_WRITE = "FINAL_WRITE";
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
import { ERR_CONNECTION_LOST } from "./errors.js";
|
||||
import { MessageBase } from "./types.js";
|
||||
import { HaWebSocket } from "./socket.js";
|
||||
import type { Auth } from "./auth.js";
|
||||
export type ConnectionOptions = {
|
||||
setupRetry: number;
|
||||
auth?: Auth;
|
||||
createSocket: (options: ConnectionOptions) => Promise<HaWebSocket>;
|
||||
};
|
||||
export type ConnectionEventListener = (conn: Connection, eventData?: any) => void;
|
||||
type Events = "ready" | "disconnected" | "reconnect-error";
|
||||
type SubscriptionUnsubscribe = () => Promise<void>;
|
||||
interface SubscribeEventCommmandInFlight<T> {
|
||||
resolve: (result?: any) => void;
|
||||
reject: (err: any) => void;
|
||||
callback: (ev: T) => void;
|
||||
subscribe: (() => Promise<SubscriptionUnsubscribe>) | undefined;
|
||||
unsubscribe: SubscriptionUnsubscribe;
|
||||
}
|
||||
type CommandWithAnswerInFlight = {
|
||||
resolve: (result?: any) => void;
|
||||
reject: (err: any) => void;
|
||||
};
|
||||
type CommandInFlight = SubscribeEventCommmandInFlight<any> | CommandWithAnswerInFlight;
|
||||
export declare class Connection {
|
||||
options: ConnectionOptions;
|
||||
commandId: number;
|
||||
commands: Map<number, CommandInFlight>;
|
||||
eventListeners: Map<string, ConnectionEventListener[]>;
|
||||
closeRequested: boolean;
|
||||
suspendReconnectPromise?: Promise<void>;
|
||||
oldSubscriptions?: Map<number, CommandInFlight>;
|
||||
_queuedMessages?: Array<{
|
||||
resolve: (value?: unknown) => unknown;
|
||||
reject?: (err: typeof ERR_CONNECTION_LOST) => unknown;
|
||||
}>;
|
||||
socket?: HaWebSocket;
|
||||
/**
|
||||
* Version string of the Home Assistant instance. Set to version of last connection while reconnecting.
|
||||
*/
|
||||
haVersion: string;
|
||||
constructor(socket: HaWebSocket, options: ConnectionOptions);
|
||||
get connected(): boolean;
|
||||
private _setSocket;
|
||||
addEventListener(eventType: Events, callback: ConnectionEventListener): void;
|
||||
removeEventListener(eventType: Events, callback: ConnectionEventListener): void;
|
||||
fireEvent(eventType: Events, eventData?: any): void;
|
||||
suspendReconnectUntil(suspendPromise: Promise<void>): void;
|
||||
suspend(): void;
|
||||
/**
|
||||
* Reconnect the websocket connection.
|
||||
* @param force discard old socket instead of gracefully closing it.
|
||||
*/
|
||||
reconnect(force?: boolean): void;
|
||||
close(): void;
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
subscribeEvents<EventType>(callback: (ev: EventType) => void, eventType?: string): Promise<SubscriptionUnsubscribe>;
|
||||
ping(): Promise<unknown>;
|
||||
sendMessage(message: MessageBase, commandId?: number): void;
|
||||
sendMessagePromise<Result>(message: MessageBase): Promise<Result>;
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
subscribeMessage<Result>(callback: (result: Result) => void, subscribeMessage: MessageBase, options?: {
|
||||
resubscribe?: boolean;
|
||||
preCheck?: () => boolean | Promise<boolean>;
|
||||
}): Promise<SubscriptionUnsubscribe>;
|
||||
private _handleMessage;
|
||||
private _handleClose;
|
||||
private _genCmdId;
|
||||
}
|
||||
export {};
|
||||
+338
@@ -0,0 +1,338 @@
|
||||
/**
|
||||
* Connection that wraps a socket and provides an interface to interact with
|
||||
* the Home Assistant websocket API.
|
||||
*/
|
||||
import * as messages from "./messages.js";
|
||||
import { ERR_INVALID_AUTH, ERR_CONNECTION_LOST } from "./errors.js";
|
||||
const DEBUG = false;
|
||||
export class Connection {
|
||||
constructor(socket, options) {
|
||||
this._handleMessage = (event) => {
|
||||
let messageGroup = JSON.parse(event.data);
|
||||
if (!Array.isArray(messageGroup)) {
|
||||
messageGroup = [messageGroup];
|
||||
}
|
||||
messageGroup.forEach((message) => {
|
||||
if (DEBUG) {
|
||||
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(messages.unsubscribeEvents(message.id)).catch((err) => {
|
||||
if (DEBUG) {
|
||||
console.warn(` Error unsubsribing from unknown subscription ${message.id}`, err);
|
||||
}
|
||||
});
|
||||
}
|
||||
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;
|
||||
default:
|
||||
if (DEBUG) {
|
||||
console.warn("Unhandled message", message);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
this._handleClose = async () => {
|
||||
const oldCommands = this.commands;
|
||||
// reset to original state except haVersion
|
||||
this.commandId = 1;
|
||||
this.oldSubscriptions = this.commands;
|
||||
this.commands = new Map();
|
||||
this.socket = undefined;
|
||||
// Reject in-flight sendMessagePromise requests
|
||||
oldCommands.forEach((info) => {
|
||||
// We don't cancel subscribeEvents commands in flight
|
||||
// as we will be able to recover them.
|
||||
if (!("subscribe" in info)) {
|
||||
info.reject(messages.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 () => {
|
||||
if (this.closeRequested) {
|
||||
return;
|
||||
}
|
||||
if (DEBUG) {
|
||||
console.log("Trying to reconnect");
|
||||
}
|
||||
try {
|
||||
const socket = await options.createSocket(options);
|
||||
this._setSocket(socket);
|
||||
}
|
||||
catch (err) {
|
||||
if (this._queuedMessages) {
|
||||
const queuedMessages = this._queuedMessages;
|
||||
this._queuedMessages = undefined;
|
||||
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) * 1000);
|
||||
};
|
||||
if (this.suspendReconnectPromise) {
|
||||
await this.suspendReconnectPromise;
|
||||
this.suspendReconnectPromise = undefined;
|
||||
// For the first retry after suspend, we will queue up
|
||||
// all messages.
|
||||
this._queuedMessages = [];
|
||||
}
|
||||
reconnect(0);
|
||||
};
|
||||
// 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 = 2; // socket may send 1 at the start to enable features
|
||||
// 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 connected() {
|
||||
// Using conn.socket.OPEN instead of WebSocket for better node support
|
||||
return (this.socket !== undefined && 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 = undefined;
|
||||
oldSubscriptions.forEach((info) => {
|
||||
if ("subscribe" in info && info.subscribe) {
|
||||
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();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
const queuedMessages = this._queuedMessages;
|
||||
if (queuedMessages) {
|
||||
this._queuedMessages = undefined;
|
||||
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, messages.subscribeEvents(eventType));
|
||||
}
|
||||
ping() {
|
||||
return this.sendMessagePromise(messages.ping());
|
||||
}
|
||||
sendMessage(message, commandId) {
|
||||
if (!this.connected) {
|
||||
throw ERR_CONNECTION_LOST;
|
||||
}
|
||||
if (DEBUG) {
|
||||
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) => {
|
||||
// Command ID that will be used
|
||||
const commandId = this._genCmdId();
|
||||
// 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: (options === null || options === void 0 ? void 0 : options.resubscribe) !== false
|
||||
? () => this.subscribeMessage(callback, subscribeMessage, options)
|
||||
: undefined,
|
||||
unsubscribe: async () => {
|
||||
// No need to unsubscribe if we're disconnected
|
||||
if (this.connected) {
|
||||
await this.sendMessagePromise(messages.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();
|
||||
}
|
||||
_genCmdId() {
|
||||
return ++this.commandId;
|
||||
}
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
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;
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
import { getCollection } from "./collection.js";
|
||||
import { getStates } from "./commands.js";
|
||||
import { atLeastHaVersion } from "./util.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 * 1000).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_changed,
|
||||
last_updated: newState.lu
|
||||
? new Date(newState.lu * 1000).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 !== undefined) {
|
||||
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 * 1000).toISOString();
|
||||
}
|
||||
else if (toAdd.lu) {
|
||||
entityState.last_updated = new Date(toAdd.lu * 1000).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);
|
||||
}
|
||||
const subscribeUpdates = (conn, store) => conn.subscribeMessage((ev) => processEvent(store, ev), {
|
||||
type: "subscribe_entities",
|
||||
});
|
||||
function legacyProcessEvent(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 legacyFetchEntities(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 legacySubscribeUpdates = (conn, store) => conn.subscribeEvents((ev) => legacyProcessEvent(store, ev), "state_changed");
|
||||
export const entitiesColl = (conn) => atLeastHaVersion(conn.haVersion, 2022, 4, 0)
|
||||
? getCollection(conn, "_ent", undefined, subscribeUpdates)
|
||||
: getCollection(conn, "_ent", legacyFetchEntities, legacySubscribeUpdates);
|
||||
export const subscribeEntities = (conn, onChange) => entitiesColl(conn).subscribe(onChange);
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
export declare const ERR_CANNOT_CONNECT = 1;
|
||||
export declare const ERR_INVALID_AUTH = 2;
|
||||
export declare const ERR_CONNECTION_LOST = 3;
|
||||
export declare const ERR_HASS_HOST_REQUIRED = 4;
|
||||
export declare const ERR_INVALID_HTTPS_TO_HTTP = 5;
|
||||
export declare const ERR_INVALID_AUTH_CALLBACK = 6;
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
export const ERR_CANNOT_CONNECT = 1;
|
||||
export const ERR_INVALID_AUTH = 2;
|
||||
export const ERR_CONNECTION_LOST = 3;
|
||||
export const ERR_HASS_HOST_REQUIRED = 4;
|
||||
export const ERR_INVALID_HTTPS_TO_HTTP = 5;
|
||||
export const ERR_INVALID_AUTH_CALLBACK = 6;
|
||||
+1124
File diff suppressed because it is too large.
Load diff
+1130
File diff suppressed because it is too large.
Load diff
+13
@@ -0,0 +1,13 @@
|
||||
import { Connection, ConnectionOptions } 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 "./socket.js";
|
||||
export * from "./types.js";
|
||||
export * from "./commands.js";
|
||||
export * from "./store.js";
|
||||
export declare function createConnection(options?: Partial<ConnectionOptions>): Promise<Connection>;
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
// JS extensions in imports allow tsc output to be consumed by browsers.
|
||||
import { createSocket } from "./socket.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 "./socket.js";
|
||||
export * from "./types.js";
|
||||
export * from "./commands.js";
|
||||
export * from "./store.js";
|
||||
export 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;
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
import { Error, HassServiceTarget } from "./types.js";
|
||||
export declare function auth(accessToken: string): {
|
||||
type: string;
|
||||
access_token: string;
|
||||
};
|
||||
export declare function supportedFeatures(): {
|
||||
type: string;
|
||||
id: number;
|
||||
features: {
|
||||
coalesce_messages: number;
|
||||
};
|
||||
};
|
||||
export declare function states(): {
|
||||
type: string;
|
||||
};
|
||||
export declare function config(): {
|
||||
type: string;
|
||||
};
|
||||
export declare function services(): {
|
||||
type: string;
|
||||
};
|
||||
export declare function user(): {
|
||||
type: string;
|
||||
};
|
||||
type ServiceCallMessage = {
|
||||
type: "call_service";
|
||||
domain: string;
|
||||
service: string;
|
||||
service_data?: object;
|
||||
target?: HassServiceTarget;
|
||||
return_response?: boolean;
|
||||
};
|
||||
export declare function callService(domain: string, service: string, serviceData?: object, target?: HassServiceTarget, returnResponse?: boolean): ServiceCallMessage;
|
||||
type SubscribeEventMessage = {
|
||||
type: "subscribe_events";
|
||||
event_type?: string;
|
||||
};
|
||||
export declare function subscribeEvents(eventType?: string): SubscribeEventMessage;
|
||||
export declare function unsubscribeEvents(subscription: number): {
|
||||
type: string;
|
||||
subscription: number;
|
||||
};
|
||||
export declare function ping(): {
|
||||
type: string;
|
||||
};
|
||||
export declare function error(code: Error, message: string): {
|
||||
type: string;
|
||||
success: boolean;
|
||||
error: {
|
||||
code: Error;
|
||||
message: string;
|
||||
};
|
||||
};
|
||||
export {};
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
export function auth(accessToken) {
|
||||
return {
|
||||
type: "auth",
|
||||
access_token: accessToken,
|
||||
};
|
||||
}
|
||||
export function supportedFeatures() {
|
||||
return {
|
||||
type: "supported_features",
|
||||
id: 1, // Always the first message after auth
|
||||
features: { coalesce_messages: 1 },
|
||||
};
|
||||
}
|
||||
export function states() {
|
||||
return {
|
||||
type: "get_states",
|
||||
};
|
||||
}
|
||||
export function config() {
|
||||
return {
|
||||
type: "get_config",
|
||||
};
|
||||
}
|
||||
export function services() {
|
||||
return {
|
||||
type: "get_services",
|
||||
};
|
||||
}
|
||||
export function user() {
|
||||
return {
|
||||
type: "auth/current_user",
|
||||
};
|
||||
}
|
||||
export 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;
|
||||
}
|
||||
export function subscribeEvents(eventType) {
|
||||
const message = {
|
||||
type: "subscribe_events",
|
||||
};
|
||||
if (eventType) {
|
||||
message.event_type = eventType;
|
||||
}
|
||||
return message;
|
||||
}
|
||||
export function unsubscribeEvents(subscription) {
|
||||
return {
|
||||
type: "unsubscribe_events",
|
||||
subscription,
|
||||
};
|
||||
}
|
||||
export function ping() {
|
||||
return {
|
||||
type: "ping",
|
||||
};
|
||||
}
|
||||
export function error(code, message) {
|
||||
return {
|
||||
type: "result",
|
||||
success: false,
|
||||
error: {
|
||||
code,
|
||||
message,
|
||||
},
|
||||
};
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import { HassServices, UnsubscribeFunc } from "./types.js";
|
||||
import { Connection } from "./connection.js";
|
||||
export declare const servicesColl: (conn: Connection) => import("./collection.js").Collection<HassServices>;
|
||||
export declare const subscribeServices: (conn: Connection, onChange: (state: HassServices) => void) => UnsubscribeFunc;
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
import { getCollection } from "./collection.js";
|
||||
import { getServices } from "./commands.js";
|
||||
import { debounce } from "./util.js";
|
||||
function processServiceRegistered(conn, store, event) {
|
||||
var _a;
|
||||
const state = store.state;
|
||||
if (state === undefined)
|
||||
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 === 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 debouncedFetchServices = debounce((conn, store) => fetchServices(conn).then((services) => store.setState(services, true)), 5000);
|
||||
const fetchServices = (conn) => getServices(conn);
|
||||
const subscribeUpdates = (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()));
|
||||
export const servicesColl = (conn) => getCollection(conn, "_srv", fetchServices, subscribeUpdates);
|
||||
export const subscribeServices = (conn, onChange) => servicesColl(conn).subscribe(onChange);
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
import type { ConnectionOptions } from "./connection.js";
|
||||
export declare const MSG_TYPE_AUTH_REQUIRED = "auth_required";
|
||||
export declare const MSG_TYPE_AUTH_INVALID = "auth_invalid";
|
||||
export declare const MSG_TYPE_AUTH_OK = "auth_ok";
|
||||
export interface HaWebSocket extends WebSocket {
|
||||
haVersion: string;
|
||||
}
|
||||
export declare function createSocket(options: ConnectionOptions): Promise<HaWebSocket>;
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* Create a web socket connection with a Home Assistant instance.
|
||||
*/
|
||||
import { ERR_INVALID_AUTH, ERR_CANNOT_CONNECT, ERR_HASS_HOST_REQUIRED, } from "./errors.js";
|
||||
import * as messages from "./messages.js";
|
||||
import { atLeastHaVersion } from "./util.js";
|
||||
const DEBUG = false;
|
||||
export const MSG_TYPE_AUTH_REQUIRED = "auth_required";
|
||||
export const MSG_TYPE_AUTH_INVALID = "auth_invalid";
|
||||
export const MSG_TYPE_AUTH_OK = "auth_ok";
|
||||
export function createSocket(options) {
|
||||
if (!options.auth) {
|
||||
throw ERR_HASS_HOST_REQUIRED;
|
||||
}
|
||||
const auth = options.auth;
|
||||
// Start refreshing expired tokens even before the WS connection is open.
|
||||
// We know that we will need auth anyway.
|
||||
let authRefreshTask = auth.expired
|
||||
? auth.refreshAccessToken().then(() => {
|
||||
authRefreshTask = undefined;
|
||||
}, () => {
|
||||
authRefreshTask = undefined;
|
||||
})
|
||||
: undefined;
|
||||
// Convert from http:// -> ws://, https:// -> wss://
|
||||
const url = auth.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);
|
||||
// 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.expired) {
|
||||
await (authRefreshTask ? authRefreshTask : auth.refreshAccessToken());
|
||||
}
|
||||
socket.send(JSON.stringify(messages.auth(auth.accessToken)));
|
||||
}
|
||||
catch (err) {
|
||||
// Refresh token failed
|
||||
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(messages.supportedFeatures()));
|
||||
}
|
||||
promResolve(socket);
|
||||
break;
|
||||
default:
|
||||
if (DEBUG) {
|
||||
// We already send response to this message when socket opens
|
||||
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));
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
import { UnsubscribeFunc } from "./types.js";
|
||||
type Listener<State> = (state: State) => void;
|
||||
type Action<State> = (state: State, ...args: any[]) => Partial<State> | Promise<Partial<State>> | null;
|
||||
type BoundAction<State> = (...args: any[]) => void;
|
||||
export type Store<State> = {
|
||||
state: State | undefined;
|
||||
action(action: Action<State>): BoundAction<State>;
|
||||
setState(update: Partial<State>, overwrite?: boolean): void;
|
||||
clearState(): void;
|
||||
subscribe(listener: Listener<State>): UnsubscribeFunc;
|
||||
};
|
||||
export declare const createStore: <State>(state?: State) => Store<State>;
|
||||
export {};
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
export 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) {
|
||||
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 = undefined;
|
||||
},
|
||||
/**
|
||||
* 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,
|
||||
};
|
||||
};
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
export type Error = 1 | 2 | 3 | 4;
|
||||
export type UnsubscribeFunc = () => void;
|
||||
export type MessageBase = {
|
||||
id?: number;
|
||||
type: string;
|
||||
[key: string]: any;
|
||||
};
|
||||
export type Context = {
|
||||
id: string;
|
||||
user_id: string | null;
|
||||
parent_id: string | null;
|
||||
};
|
||||
export type HassEventBase = {
|
||||
origin: string;
|
||||
time_fired: string;
|
||||
context: Context;
|
||||
};
|
||||
export type HassEvent = HassEventBase & {
|
||||
event_type: string;
|
||||
data: {
|
||||
[key: string]: any;
|
||||
};
|
||||
};
|
||||
export type StateChangedEvent = HassEventBase & {
|
||||
event_type: "state_changed";
|
||||
data: {
|
||||
entity_id: string;
|
||||
new_state: HassEntity | null;
|
||||
old_state: HassEntity | null;
|
||||
};
|
||||
};
|
||||
export type HassConfig = {
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
elevation: number;
|
||||
radius: number;
|
||||
unit_system: {
|
||||
length: string;
|
||||
mass: string;
|
||||
volume: string;
|
||||
temperature: string;
|
||||
pressure: string;
|
||||
wind_speed: string;
|
||||
accumulated_precipitation: string;
|
||||
};
|
||||
location_name: string;
|
||||
time_zone: string;
|
||||
components: string[];
|
||||
config_dir: string;
|
||||
allowlist_external_dirs: string[];
|
||||
allowlist_external_urls: string[];
|
||||
version: string;
|
||||
config_source: string;
|
||||
recovery_mode: boolean;
|
||||
safe_mode: boolean;
|
||||
state: "NOT_RUNNING" | "STARTING" | "RUNNING" | "STOPPING" | "FINAL_WRITE";
|
||||
external_url: string | null;
|
||||
internal_url: string | null;
|
||||
currency: string;
|
||||
country: string | null;
|
||||
language: string;
|
||||
};
|
||||
export type HassEntityBase = {
|
||||
entity_id: string;
|
||||
state: string;
|
||||
last_changed: string;
|
||||
last_updated: string;
|
||||
attributes: HassEntityAttributeBase;
|
||||
context: Context;
|
||||
};
|
||||
export type HassEntityAttributeBase = {
|
||||
friendly_name?: string;
|
||||
unit_of_measurement?: string;
|
||||
icon?: string;
|
||||
entity_picture?: string;
|
||||
supported_features?: number;
|
||||
hidden?: boolean;
|
||||
assumed_state?: boolean;
|
||||
device_class?: string;
|
||||
state_class?: string;
|
||||
restored?: boolean;
|
||||
};
|
||||
export type HassEntity = HassEntityBase & {
|
||||
attributes: {
|
||||
[key: string]: any;
|
||||
};
|
||||
};
|
||||
export type HassEntities = {
|
||||
[entity_id: string]: HassEntity;
|
||||
};
|
||||
export type HassService = {
|
||||
name?: string;
|
||||
description?: string;
|
||||
description_placeholders?: {
|
||||
[placeholder: string]: string;
|
||||
};
|
||||
target?: {} | null;
|
||||
fields: {
|
||||
[field_name: string]: {
|
||||
example?: string | boolean | number;
|
||||
default?: unknown;
|
||||
required?: boolean;
|
||||
advanced?: boolean;
|
||||
selector?: {};
|
||||
filter?: {
|
||||
supported_features?: number[];
|
||||
attribute?: Record<string, any[]>;
|
||||
};
|
||||
name?: string;
|
||||
description?: string;
|
||||
};
|
||||
};
|
||||
response?: {
|
||||
optional: boolean;
|
||||
};
|
||||
};
|
||||
export type HassDomainServices = {
|
||||
[service_name: string]: HassService;
|
||||
};
|
||||
export type HassServices = {
|
||||
[domain: string]: HassDomainServices;
|
||||
};
|
||||
export type HassUser = {
|
||||
id: string;
|
||||
is_admin: boolean;
|
||||
is_owner: boolean;
|
||||
name: string;
|
||||
};
|
||||
export type HassServiceTarget = {
|
||||
entity_id?: string | string[];
|
||||
device_id?: string | string[];
|
||||
area_id?: string | string[];
|
||||
floor_id?: string | string[];
|
||||
label_id?: string | string[];
|
||||
};
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
// This file has no imports on purpose
|
||||
// So it can easily be consumed by other TS projects
|
||||
export {};
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
export declare function parseQuery<T>(queryString: string): T;
|
||||
export declare const debounce: <T extends (...args: any[]) => unknown>(func: T, wait: number, immediate?: boolean) => T;
|
||||
export declare const atLeastHaVersion: (version: string, major: number, minor: number, patch?: number) => boolean;
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
export 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;
|
||||
}
|
||||
// From: https://davidwalsh.name/javascript-debounce-function
|
||||
// Returns a function, that, as long as it continues to be invoked, will not
|
||||
// be triggered. The function will be called after it stops being called for
|
||||
// N milliseconds. If `immediate` is passed, trigger the function on the
|
||||
// leading edge, instead of the trailing.
|
||||
// eslint-disable-next-line: ban-types
|
||||
export const debounce = (func, wait, immediate = false) => {
|
||||
let timeout;
|
||||
// @ts-ignore
|
||||
return function (...args) {
|
||||
// @ts-ignore
|
||||
const context = this;
|
||||
const later = () => {
|
||||
timeout = undefined;
|
||||
if (!immediate) {
|
||||
func.apply(context, args);
|
||||
}
|
||||
};
|
||||
const callNow = immediate && !timeout;
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
if (callNow) {
|
||||
func.apply(context, args);
|
||||
}
|
||||
};
|
||||
};
|
||||
export const atLeastHaVersion = (version, major, minor, patch) => {
|
||||
const [haMajor, haMinor, haPatch] = version.split(".", 3);
|
||||
return (Number(haMajor) > major ||
|
||||
(Number(haMajor) === major &&
|
||||
(patch === undefined
|
||||
? Number(haMinor) >= minor
|
||||
: Number(haMinor) > minor)) ||
|
||||
(patch !== undefined &&
|
||||
Number(haMajor) === major &&
|
||||
Number(haMinor) === minor &&
|
||||
Number(haPatch) >= patch));
|
||||
};
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"name": "home-assistant-js-websocket",
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
"version": "9.6.0",
|
||||
"description": "Home Assistant websocket client",
|
||||
"source": "lib/index.ts",
|
||||
"types": "dist/index.d.ts",
|
||||
"main": "dist/haws.umd.js",
|
||||
"module": "dist/index.js",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/index.js",
|
||||
"require": "./dist/haws.cjs",
|
||||
"default": "./dist/haws.umd.js"
|
||||
},
|
||||
"./haws.cjs": "./dist/haws.cjs",
|
||||
"./haws.umd.js": "./dist/haws.umd.js",
|
||||
"./dist/*": "./dist/*"
|
||||
},
|
||||
"repository": {
|
||||
"url": "https://github.com/home-assistant/home-assistant-js-websocket.git",
|
||||
"type": "git"
|
||||
},
|
||||
"scripts": {
|
||||
"watch": "tsc --watch",
|
||||
"build": "tsc && rollup -c",
|
||||
"test": "tsc && prettier --check . && mocha --loader=ts-node/esm",
|
||||
"format": "prettier --write .",
|
||||
"prepublishOnly": "rm -rf dist && yarn build && yarn test"
|
||||
},
|
||||
"author": "Paulus Schoutsen <paulus@paulusschoutsen.nl>",
|
||||
"license": "Apache-2.0",
|
||||
"devDependencies": {
|
||||
"@types/assert": "^1.4.7",
|
||||
"@types/mocha": "^10.0.9",
|
||||
"assert": "^2.0.0",
|
||||
"husky": "^4.2.5",
|
||||
"lint-staged": "^16.0.0",
|
||||
"mocha": "^11.1.0",
|
||||
"prettier": "^3.0.0",
|
||||
"rollup": "^4.3.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.2.2"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "lint-staged"
|
||||
}
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{ts,js,json,css,md}": [
|
||||
"prettier --write"
|
||||
]
|
||||
},
|
||||
"packageManager": "yarn@4.7.0"
|
||||
}
|
||||
Reference in new issue
Block a user