Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 127 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,53 @@ client.on('logs', ({ message }) => {
console.log(message);
});
```

### User-defined services (Synopsis)
[User-defined services](https://esphome.io/components/api.html#user-defined-services) let an ESPHome device expose custom callable functions.
Use `listEntitiesService()` to discover them and `executeServiceService()` to invoke them.

```javascript
const { Connection, ServiceArgType } = require('@2colors/esphome-native-api');

const connection = new Connection({
host: '<esp host or ip>',
port: 6053,
});

connection.connect();

connection.on('authorized', async () => {
// Discover all entities, including user-defined services
const entities = await connection.listEntitiesService();

const services = entities
.filter(e => e.component === 'Services')
.map(e => e.entity);
console.log('User-defined services:', services);
/*
[
{
name: 'my_service',
key: 1234567890,
argsList: [
{ name: 'brightness', type: 1 }, // ServiceArgType.Int
{ name: 'message', type: 3 } // ServiceArgType.String
]
}
]
*/

// Call a user-defined service by key with typed arguments
connection.executeServiceService({
key: services[0].key,
args: [
Comment thread
DutchmanNL marked this conversation as resolved.
{ type: ServiceArgType.Int, value: 100 },
{ type: ServiceArgType.String, value: 'hello' },
],
});
});
```

## Documantation

### Discovery
Expand Down Expand Up @@ -283,6 +330,83 @@ Only base functionality
- `state` - REQUIRED. string. See `minLength`, `maxLength` attrs in config


### User-defined Services
[User-defined services](https://esphome.io/components/api.html#user-defined-services) are custom callable functions declared in the ESPHome device's YAML configuration. They are discovered automatically when you call `listEntitiesService()` and are invoked with `executeServiceService()`.

#### ServiceArgType
The `ServiceArgType` export maps argument type names to the numeric values used in the protocol:

| Name | Value | Description |
|---------------|-------|-----------------------|
| `Bool` | 0 | Single boolean |
| `Int` | 1 | Single integer |
| `Float` | 2 | Single float |
| `String` | 3 | Single string |
| `BoolArray` | 4 | Array of booleans |
| `IntArray` | 5 | Array of integers |
| `FloatArray` | 6 | Array of floats |
| `StringArray` | 7 | Array of strings |

```javascript
const { ServiceArgType } = require('@2colors/esphome-native-api');
```

#### Discovering services with `listEntitiesService()`
Services are returned alongside regular entities. Filter by `component === 'Services'` to get only service definitions:

```javascript
const entities = await connection.listEntitiesService();
const services = entities
.filter(e => e.component === 'Services')
.map(e => e.entity);
/*
[
{
name: 'my_service',
key: 1234567890,
argsList: [
{ name: 'brightness', type: 1 }, // ServiceArgType.Int
{ name: 'enable', type: 0 } // ServiceArgType.Bool
]
}
]
*/
```

Each service entry has:
- `name` - string. The service name as declared in ESPHome YAML
- `key` - number. Unique key used to invoke the service
- `argsList` - array of `{ name: string, type: ServiceArgType }` argument descriptors
Comment thread
DutchmanNL marked this conversation as resolved.

#### Executing a service with `executeServiceService(data)`
`connection.executeServiceService({ key, args })`

- `key` - REQUIRED. number. The service `key` from the discovery response
- `args` - optional. Array of `{ type: ServiceArgType, value }` objects, one per argument in the order declared by `argsList`

```javascript
const { Connection, ServiceArgType } = require('@2colors/esphome-native-api');

connection.executeServiceService({
key: 1234567890,
args: [
{ type: ServiceArgType.Int, value: 100 },
{ type: ServiceArgType.Bool, value: true },
{ type: ServiceArgType.String, value: 'hello' },
],
});
```

#### Listening for service definitions
You can also react to service definitions as they arrive using the connection event:

```javascript
connection.on('message.ListEntitiesServicesResponse', (service) => {
console.log('Service discovered:', service.name, 'key:', service.key);
});
```


### Connection
```javascript
const { Connection } = require('@2colors/esphome-native-api');
Expand Down Expand Up @@ -347,6 +471,9 @@ const connection = new Connection({
- `sirenCommandService(data)`
- `switchCommandService(data)`
- `mediaplayerCommandService(data)`
- `executeServiceService({ key, args })` - executes a [user-defined service](https://esphome.io/components/api.html#user-defined-services). See [User-defined Services](#user-defined-services) for full details.
- `key` - REQUIRED. number. Service key obtained from `listEntitiesService()`
- `args` - optional. Array of `{ type: ServiceArgType, value }` objects

#### Connection events
- `message.<type>` - when valid message from esphome device is received. First arg is message. The event is called before `message` event(more genetal analogue)
Expand Down
53 changes: 51 additions & 2 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,43 @@ declare module "@2colors/esphome-native-api" {
mode: TextMode;
};

export enum ServiceArgType {
Bool = 0,
Int = 1,
Float = 2,
String = 3,
BoolArray = 4,
IntArray = 5,
FloatArray = 6,
StringArray = 7,
}

export type ListEntitiesServicesArgument = {
name: string;
type: ServiceArgType;
};

export type ListEntitiesServicesResponse = {
name: string;
key: number;
argsList: ListEntitiesServicesArgument[];
};

export type ExecuteServiceArgument =
| { type: ServiceArgType.Bool; value: boolean }
| { type: ServiceArgType.Int; value: number }
| { type: ServiceArgType.Float; value: number }
| { type: ServiceArgType.String; value: string }
| { type: ServiceArgType.BoolArray; value: boolean[] }
| { type: ServiceArgType.IntArray; value: number[] }
| { type: ServiceArgType.FloatArray; value: number[] }
| { type: ServiceArgType.StringArray; value: string[] };

export type ExecuteServiceCommandData = {
key: number;
args?: ExecuteServiceArgument[];
};

type Components =
| "BinarySensor"
| "Cover"
Expand All @@ -238,7 +275,8 @@ declare module "@2colors/esphome-native-api" {
| "Lock"
| "Button"
| "MediaPlayer"
| "Text";
| "Text"
| "Services";

type Entities =
| ListEntitiesEntityResponse
Expand All @@ -255,7 +293,8 @@ declare module "@2colors/esphome-native-api" {
| ListEntitiesLockResponse
| ListEntitiesButtonResponse
| ListEntitiesMediaPlayerResponse
| ListEntitiesTextResponse;
| ListEntitiesTextResponse
| ListEntitiesServicesResponse;

export type EntityList = {
component: Components;
Expand Down Expand Up @@ -532,6 +571,7 @@ declare module "@2colors/esphome-native-api" {
switchCommandService(data: SwitchCommandData): void;
mediaPlayerCommandService(data: MediaPlayerCommandData): void;
textCommandService(data: TextCommandData): void;
executeServiceService(data: ExecuteServiceCommandData): void;

subscribeBluetoothAdvertisementService(): void;
unsubscribeBluetoothAdvertisementService(): void;
Expand Down Expand Up @@ -764,6 +804,15 @@ declare module "@2colors/esphome-native-api" {
listener: (message: ListEntitiesTextResponse) => void
): this;

on(
event: "message.ListEntitiesServicesResponse",
listener: (message: ListEntitiesServicesResponse) => void
): this;
off(
event: "message.ListEntitiesServicesResponse",
listener: (message: ListEntitiesServicesResponse) => void
): this;

on(event: string, listener: (...args: any[]) => void): this;
off(event: string, listener: (...args: any[]) => void): this;
}
Expand Down
17 changes: 15 additions & 2 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,23 @@
const Connection = require('./lib/connection');
const Client = require('./lib/client');
const Discovery = require('./lib/discovery');
const { pb } = require('./lib/utils/messages');

const ServiceArgType = {
Bool: pb.ServiceArgType.SERVICE_ARG_TYPE_BOOL,
Int: pb.ServiceArgType.SERVICE_ARG_TYPE_INT,
Float: pb.ServiceArgType.SERVICE_ARG_TYPE_FLOAT,
String: pb.ServiceArgType.SERVICE_ARG_TYPE_STRING,
BoolArray: pb.ServiceArgType.SERVICE_ARG_TYPE_BOOL_ARRAY,
IntArray: pb.ServiceArgType.SERVICE_ARG_TYPE_INT_ARRAY,
FloatArray: pb.ServiceArgType.SERVICE_ARG_TYPE_FLOAT_ARRAY,
StringArray: pb.ServiceArgType.SERVICE_ARG_TYPE_STRING_ARRAY,
};

module.exports = {
Connection,
Client,
Discovery,
discovery: Discovery
}
discovery: Discovery,
ServiceArgType
}
43 changes: 42 additions & 1 deletion lib/connection.js
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,8 @@ class EsphomeNativeApiConnection extends EventEmitter {
'ListEntitiesLockResponse',
'ListEntitiesButtonResponse',
'ListEntitiesMediaPlayerResponse',
'ListEntitiesTextResponse'
'ListEntitiesTextResponse',
'ListEntitiesServicesResponse'
]
const entitiesList = [];
const onMessage = (type, message) => {
Expand Down Expand Up @@ -489,6 +490,46 @@ class EsphomeNativeApiConnection extends EventEmitter {
textCommandService(data) {
Entities.Text.commandService(this, data);
}
executeServiceService({ key, args = [] }) {
if (!this.connected) throw new Error(`Not connected`);
if (!this.authorized) throw new Error(`Not authorized`);
const message = new pb.ExecuteServiceRequest();
message.setKey(key);
for (const { type, value } of args) {
const arg = new pb.ExecuteServiceArgument();
switch (type) {
case pb.ServiceArgType.SERVICE_ARG_TYPE_BOOL:
arg.setBool(value);
break;
case pb.ServiceArgType.SERVICE_ARG_TYPE_INT:
arg.setInt(value);
Comment thread
DutchmanNL marked this conversation as resolved.
arg.setLegacyInt(value);
break;
case pb.ServiceArgType.SERVICE_ARG_TYPE_FLOAT:
arg.setFloat(value);
break;
case pb.ServiceArgType.SERVICE_ARG_TYPE_STRING:
arg.setString(value);
break;
case pb.ServiceArgType.SERVICE_ARG_TYPE_BOOL_ARRAY:
arg.setBoolArrayList(value);
break;
case pb.ServiceArgType.SERVICE_ARG_TYPE_INT_ARRAY:
arg.setIntArrayList(value);
break;
case pb.ServiceArgType.SERVICE_ARG_TYPE_FLOAT_ARRAY:
arg.setFloatArrayList(value);
break;
case pb.ServiceArgType.SERVICE_ARG_TYPE_STRING_ARRAY:
arg.setStringArrayList(value);
break;
default:
throw new Error(`Unknown service arg type: ${type}`);
}
message.addArgs(arg);
}
this.sendCommandMessage(message);
}
}

module.exports = EsphomeNativeApiConnection;