Skip to content

Redesign user-defined ESPHome services using executeServiceService from @2colors/esphome-native-api 1.3.6 - #415

Draft
DutchmanNL with Copilot wants to merge 3 commits into
mainfrom
copilot/redesign-user-defined-services
Draft

Redesign user-defined ESPHome services using executeServiceService from @2colors/esphome-native-api 1.3.6#415
DutchmanNL with Copilot wants to merge 3 commits into
mainfrom
copilot/redesign-user-defined-services

Conversation

Copilot AI commented Feb 25, 2026

Copy link
Copy Markdown
Contributor

PR #406 implemented user-defined ESPHome API services via a direct pb import workaround (@2colors/esphome-native-api/lib/utils/messages). Library v1.3.6 now exposes connection.executeServiceService() as a proper public API, making the workaround unnecessary.

Changes

  • Dependency: Bumped @2colors/esphome-native-api ^1.3.5^1.3.6
  • lib/helpers.js: Added userDefinedServices = {} to DeviceInfo class to cache per-device service configs
  • Service discovery (main.js): Listens for message.ListEntitiesServicesResponse on the connection; stores listener reference for .off() cleanup on disconnect
  • Reconnect hygiene: On connected, resets userDefinedServices = {} and strips UserDefinedServices channels from the tracking array so objectCleanup can prune services removed from device YAML
  • handleUserDefinedService(): Builds ioBroker state tree DeviceName.UserDefinedServices.<key>.<argName> + a run button per service
  • executeUserDefinedService(): Replaces manual protobuf construction with the new library call:
// Before (workaround) — imported pb from internal path, built request manually
const { pb } = require('@2colors/esphome-native-api/lib/utils/messages');
const request = new pb.ExecuteServiceRequest();
request.setKey(Number(serviceKey));
// ...build args manually...
clientDetails[deviceIP].client.connection.sendCommandMessage(request);

// After — clean public API
clientDetails[deviceIP].client.connection.executeServiceService({ key: Number(serviceKey), args });
  • onStateChange: Detects writes to UserDefinedServices states; triggers execution on run button press, validates and acknowledges argument value changes
  • parseServiceArrayArg(): Helper for JSON-encoded array arguments (BoolArray, IntArray, FloatArray, StringArray types)

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • 224.0.0.251
    • Triggering command: REDACTED, pid is -1 (packet block)
  • https://api.github.com/repos/esphome/esphome/releases
    • Triggering command: /home/REDACTED/work/_temp/ghcca-node/node/bin/node node main.js --console sh postinstall stdout.buffer.write(sys.executable.encode(&#39;utf-8&#39;)); (http block)
    • Triggering command: /home/REDACTED/work/_temp/ghcca-node/node/bin/node node main.js --console 13.1/deps/openssl/openssl/include /nod�� 13.1/deps/uv/include (http block)
    • Triggering command: /home/REDACTED/work/_temp/ghcca-node/node/bin/node node main.js --console (http block)
  • https://api.github.com/repos/indygreg/python-build-standalone/releases
    • Triggering command: /home/REDACTED/work/_temp/ghcca-node/node/bin/node node main.js --console 13.1/deps/openssl/openssl/include /nod�� 13.1/deps/uv/include (http block)
    • Triggering command: /home/REDACTED/work/_temp/ghcca-node/node/bin/node node main.js --console (http block)

If you need me to access, download, or install something from one of these locations, you can either:

Original prompt

This section details on the original issue you should resolve

<issue_title>Implement user defined services</issue_title>
<issue_description>Pull request #406 implements user defined services solving #117 and #251

Current solution (not merged) implements a direct lib workaround.
As those functions are in meantime already implemented in the new version 1.3.6 of library https://github.com/twocolors/esphome-native-api this solution should be redesigned using the new exposed functions.</issue_description>

Comments on the Issue (you are @copilot in this section)


🔒 GitHub Advanced Security automatically protects Copilot coding agent pull requests. You can protect all pull requests by enabling Advanced Security for your repositories. Learn more about Advanced Security.

…rom @2colors/esphome-native-api 1.3.6

Co-authored-by: DutchmanNL <7318445+DutchmanNL@users.noreply.github.com>
Copilot AI changed the title [WIP] Update implementation of user defined services using new library functions Redesign user-defined ESPHome services using executeServiceService from @2colors/esphome-native-api 1.3.6 Feb 25, 2026
@DutchmanNL
DutchmanNL marked this pull request as ready for review February 25, 2026 21:27
Copilot AI review requested due to automatic review settings February 25, 2026 21:27
@DutchmanNL

Copy link
Copy Markdown
Contributor

@SimonFischer04 the proper implementation for #406 based on the lib

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the adapter’s user-defined ESPHome service support to use the newly exposed public API in @2colors/esphome-native-api@1.3.6, replacing the prior internal/protobuf workaround and improving reconnect/disconnect hygiene.

Changes:

  • Bump @2colors/esphome-native-api dependency to ^1.3.6.
  • Add discovery + execution flow for user-defined services via message.ListEntitiesServicesResponse and connection.executeServiceService(...).
  • Track/cleanup UserDefinedServices channels and cached service configs across reconnects.

Reviewed changes

Copilot reviewed 4 out of 5 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
package.json Bumps @2colors/esphome-native-api to ^1.3.6 to use the new public service execution API.
package-lock.json Locks dependency update to @2colors/esphome-native-api@1.3.6.
main.js Implements service discovery listener, object tree creation for services/args, execution via executeServiceService, and reconnect/disconnect cleanup logic.
lib/helpers.js Adds userDefinedServices cache to DeviceInfo for per-device service configs.
README.md Adds WORK IN PROGRESS changelog entry describing the user-defined services feature.

Comment thread main.js
Comment on lines +2568 to +2569
clientDetails[deviceIP].client.connection.executeServiceService({ key: Number(serviceKey), args });
this.log.info(`Executed user-defined service "${serviceConfig.name}" on ${deviceName}`);

Copilot AI Feb 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

executeUserDefinedService() calls connection.executeServiceService(...) without await/error handling. If the library method returns a Promise (likely), failures can become unhandled rejections and the subsequent “Executed …” log line can be misleading. await the call and wrap it in try/catch to log/propagate execution errors appropriately.

Suggested change
clientDetails[deviceIP].client.connection.executeServiceService({ key: Number(serviceKey), args });
this.log.info(`Executed user-defined service "${serviceConfig.name}" on ${deviceName}`);
try {
await clientDetails[deviceIP].client.connection.executeServiceService({ key: Number(serviceKey), args });
this.log.info(`Executed user-defined service "${serviceConfig.name}" on ${deviceName}`);
} catch (error) {
this.log.error(
`Failed to execute user-defined service "${serviceConfig.name}" on ${deviceName}: ${
error && error.message ? error.message : error
}`,
);
}

Copilot uses AI. Check for mistakes.
Comment thread main.js
native: {},
});
this.subscribeStates(`${serviceChannel}.${arg.name}`);
await this.stateSetCreate(`${serviceChannel}.${arg.name}`, arg.name, iobDef);

Copilot AI Feb 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

handleUserDefinedService() creates argument states with write: true, but then calls stateSetCreate(..., arg.name, iobDef) without the writable parameter. stateSetCreate defaults common.write to false and will (re)extend the object as read-only, which can prevent users/scripts from writing argument values in ioBroker. Pass true for the writable argument (and ideally provide initialStateCommon for role/type/def) or avoid stateSetCreate here and just setStateAsync the default value after extendObjectAsync.

Suggested change
await this.stateSetCreate(`${serviceChannel}.${arg.name}`, arg.name, iobDef);
await this.stateSetCreate(`${serviceChannel}.${arg.name}`, arg.name, iobDef, true);

Copilot uses AI. Check for mistakes.
Comment thread main.js
Comment on lines +2411 to +2416
const deviceName = clientDetails[host].deviceName;
if (!deviceName) {
this.log.warn(
`[handleUserDefinedService] deviceName not yet known for ${host}, skipping service "${serviceConfig.name}"`,
);
return;

Copilot AI Feb 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

handleUserDefinedService() bails out when deviceName is not yet known and drops the service announcement entirely. Since the service listener is registered before the deviceInfo handler sets clientDetails[host].deviceName, this can cause user-defined services to never be created after connect. Consider caching serviceConfig per host until deviceInfo arrives (then process the backlog), or using a stable ID based on host until deviceName is available.

Suggested change
const deviceName = clientDetails[host].deviceName;
if (!deviceName) {
this.log.warn(
`[handleUserDefinedService] deviceName not yet known for ${host}, skipping service "${serviceConfig.name}"`,
);
return;
let deviceName = clientDetails[host] && clientDetails[host].deviceName;
if (!deviceName) {
// Fall back to a stable identifier based on host to avoid dropping the service announcement
deviceName = String(host)
.replace(/[:.]/g, '_')
.trim();
this.log.warn(
`[handleUserDefinedService] deviceName not yet known for ${host}, using fallback name "${deviceName}" for service "${serviceConfig.name}"`,
);

Copilot uses AI. Check for mistakes.
Comment thread main.js
def: false,
},
native: {},
});

Copilot AI Feb 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The run state is created via extendObjectAsync but never initialized/tracked via stateSetCreate (unlike the argument states). This leaves the state value potentially null until first use and also means objectCleanup() will treat it as an “unknown state” (because it isn’t in createdStatesDetails), causing confusing debug output. Initialize it (e.g., via stateSetCreate(..., 'run', false, undefined, true, { role: 'button', read: false, write: true })) or at least setStateAsync(..., {val:false, ack:true}) after creation and ensure it’s recorded in createdStatesDetails.

Suggested change
});
});
await this.stateSetCreate(
`${serviceChannel}.run`,
`Run ${serviceConfig.name}`,
false,
undefined,
true,
{ role: 'button', read: false, write: true },
);

Copilot uses AI. Check for mistakes.

@SimonFischer04 SimonFischer04 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. It does not solve #251. #251 was meant about sensor: - platform: homeassistant. Not services - which had an existing issue.
    Gave the issue a bad title. Changed that now. Esphome is confusing with all their terminology...

  2. Esphome now refers to services as actions. Update code to also refer to them as actions everywhere to avoid user confusion.
    See: https://developers.home-assistant.io/blog/2024/07/16/service-actions/


Other than that: already said a few times: main is getting way to crowded. Very hard to properly review this. But should not break existing stuff so fine to merge.

@DutchmanNL

Copy link
Copy Markdown
Contributor
  1. It does not solve Support HA (Sensor) Platform / Dedicated iobroker platform. #251. Support HA (Sensor) Platform / Dedicated iobroker platform. #251 was meant about sensor: - platform: homeassistant. Not services - which had an existing issue.
    Gave the issue a bad title. Changed that now. Esphome is confusing with all their terminology...
  2. Esphome now refers to services as actions. Update code to also refer to them as actions everywhere to avoid user confusion.
    See: https://developers.home-assistant.io/blog/2024/07/16/service-actions/

Other than that: already said a few times: main is getting way to crowded. Very hard to properly review this. But should not break existing stuff so fine to merge.

i will keep the PR for inspiration but lets implement it after rework

@SimonFischer04
SimonFischer04 marked this pull request as draft April 8, 2026 07:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement user defined services

4 participants