v2c-any 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +230 -0
  3. package/dist/adapter/adapter.js +1 -0
  4. package/dist/application-context.js +31 -0
  5. package/dist/configuration/configuration-loader.js +48 -0
  6. package/dist/configuration/configuration-validator.js +24 -0
  7. package/dist/device/shelly-pro-em/em1-status-provider.js +49 -0
  8. package/dist/device/shelly-pro-em/energy-information-em1-status-adapter.js +24 -0
  9. package/dist/device/shelly-pro-em/index.js +8 -0
  10. package/dist/factory/em1-status-provider-factory.js +48 -0
  11. package/dist/factory/executable-service-factory.js +1 -0
  12. package/dist/factory/mqtt-feed-executable-service-factory.js +68 -0
  13. package/dist/factory/mqtt-pull-executable-service-factory.js +73 -0
  14. package/dist/factory/mqtt-push-executable-service-factory.js +36 -0
  15. package/dist/factory/mqtt-service-factory.js +61 -0
  16. package/dist/factory/rest-service-factory.js +35 -0
  17. package/dist/index.js +53 -0
  18. package/dist/provider/adapter-provider.js +54 -0
  19. package/dist/provider/factory.js +1 -0
  20. package/dist/provider/fixed-value-provider.js +54 -0
  21. package/dist/provider/provider-factory.js +1 -0
  22. package/dist/provider/provider.js +1 -0
  23. package/dist/registry/registry.js +27 -0
  24. package/dist/schema/configuration.js +7 -0
  25. package/dist/schema/expectation-body.js +15 -0
  26. package/dist/schema/mqtt-configuration.js +62 -0
  27. package/dist/schema/rest-configuration.js +73 -0
  28. package/dist/schema/status-query.js +7 -0
  29. package/dist/service/executable-service.js +1 -0
  30. package/dist/service/mqtt-bridge-service.js +55 -0
  31. package/dist/service/mqtt-service.js +79 -0
  32. package/dist/service/no-op-executable-service.js +25 -0
  33. package/dist/service/pull-push-service.js +81 -0
  34. package/dist/service/rest-service.js +107 -0
  35. package/dist/template/response.js +8 -0
  36. package/dist/utils/callback-properties.js +1 -0
  37. package/dist/utils/logger.js +15 -0
  38. package/dist/utils/mappers.js +18 -0
  39. package/dist/utils/mqtt-callbacks.js +1 -0
  40. package/dist/utils/mqtt.js +21 -0
  41. package/package.json +50 -0
@@ -0,0 +1,61 @@
1
+ import { MqttService } from '../service/mqtt-service.js';
2
+ /**
3
+ * Factory for creating an MQTT service that manages energy publishing.
4
+ * Composes MQTT client setup with grid and solar feed publishers,
5
+ * exposing a unified executable service with start/stop lifecycle.
6
+ */
7
+ export class MqttServiceFactory {
8
+ /**
9
+ * Creates a new MQTT service factory.
10
+ * @param mqttFeedExecutableServiceFactory - Factory to build feed publishers (pull/push) for energy data
11
+ */
12
+ constructor(mqttFeedExecutableServiceFactory) {
13
+ this.mqttFeedExecutableServiceFactory = mqttFeedExecutableServiceFactory;
14
+ }
15
+ /**
16
+ * Creates an executable MQTT service wired to publish grid and solar power.
17
+ * Initializes the MQTT client and constructs feed publishers based on configuration.
18
+ *
19
+ * @param configuration - MQTT provider configuration including broker URL, device, and meter feeds
20
+ * @returns An ExecutableService with coordinated start/stop for MQTT client and feed publishers
21
+ */
22
+ create(configuration) {
23
+ const mqttService = new MqttService({ url: configuration.properties.url });
24
+ const callbacks = {
25
+ grid: {
26
+ callback: async (power) => {
27
+ await mqttService.pushGridPower(power);
28
+ },
29
+ },
30
+ sun: {
31
+ callback: async (power) => {
32
+ await mqttService.pushSunPower(power);
33
+ },
34
+ },
35
+ };
36
+ const gridEnergyPublisher = this.mqttFeedExecutableServiceFactory.create({
37
+ configuration: configuration.properties.meters.grid,
38
+ device: configuration.properties.device,
39
+ energyType: 'grid',
40
+ callbacks,
41
+ });
42
+ const sunEnergyPublisher = this.mqttFeedExecutableServiceFactory.create({
43
+ configuration: configuration.properties.meters.solar,
44
+ device: configuration.properties.device,
45
+ energyType: 'solar',
46
+ callbacks,
47
+ });
48
+ return {
49
+ async start() {
50
+ await mqttService.start();
51
+ await gridEnergyPublisher.start();
52
+ await sunEnergyPublisher.start();
53
+ },
54
+ async stop() {
55
+ await sunEnergyPublisher.stop();
56
+ await gridEnergyPublisher.stop();
57
+ await mqttService.stop();
58
+ },
59
+ };
60
+ }
61
+ }
@@ -0,0 +1,35 @@
1
+ import { RestService } from '../service/rest-service.js';
2
+ /**
3
+ * Factory for creating REST services that expose energy data.
4
+ * Composes grid and solar energy providers based on configuration and
5
+ * returns an executable REST service instance.
6
+ */
7
+ export class RestServiceFactory {
8
+ /**
9
+ * Creates a new REST service factory.
10
+ * @param em1StatusProviderFactory - Factory used to build device-specific energy providers
11
+ */
12
+ constructor(em1StatusProviderFactory) {
13
+ this.em1StatusProviderFactory = em1StatusProviderFactory;
14
+ }
15
+ /**
16
+ * Creates an executable REST service using configured grid and solar providers.
17
+ * @param configuration - REST provider configuration including device and meter setups
18
+ * @returns An ExecutableService that serves energy data over HTTP
19
+ */
20
+ create(configuration) {
21
+ const gridEnergyProvider = this.em1StatusProviderFactory.create({
22
+ energyType: 'grid',
23
+ device: configuration.properties.device,
24
+ configuration: configuration.properties.meters.grid,
25
+ });
26
+ const solarEnergyProvider = this.em1StatusProviderFactory.create({
27
+ energyType: 'solar',
28
+ device: configuration.properties.device,
29
+ configuration: configuration.properties.meters.solar,
30
+ });
31
+ return new RestService(gridEnergyProvider, solarEnergyProvider, {
32
+ port: configuration.properties.port,
33
+ });
34
+ }
35
+ }
package/dist/index.js ADDED
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env node
2
+ import { devicesAdapterRegistry, devicesProviderRegistry, loadDeviceModules, } from './application-context.js';
3
+ import { ConfigurationLoader } from './configuration/configuration-loader.js';
4
+ import { ConfigurationValidator } from './configuration/configuration-validator.js';
5
+ import { logger } from './utils/logger.js';
6
+ import { MqttFeedExecutableServiceFactory } from './factory/mqtt-feed-executable-service-factory.js';
7
+ import { EM1StatusProviderFactory } from './factory/em1-status-provider-factory.js';
8
+ import { MqttPullExecutableServiceFactory } from './factory/mqtt-pull-executable-service-factory.js';
9
+ import { MqttPushExecutableServiceFactory } from './factory/mqtt-push-executable-service-factory.js';
10
+ import { RestServiceFactory } from './factory/rest-service-factory.js';
11
+ import { MqttServiceFactory } from './factory/mqtt-service-factory.js';
12
+ async function main() {
13
+ let service = null;
14
+ await loadDeviceModules();
15
+ const configurationValidator = new ConfigurationValidator();
16
+ const configurationLoader = new ConfigurationLoader(configurationValidator);
17
+ const configuration = await configurationLoader.load();
18
+ switch (configuration.provider) {
19
+ case 'rest': {
20
+ const emulatorFactory = new RestServiceFactory(new EM1StatusProviderFactory(devicesProviderRegistry));
21
+ service = emulatorFactory.create(configuration);
22
+ break;
23
+ }
24
+ case 'mqtt': {
25
+ const mqttFeedExecutableServiceFactory = new MqttFeedExecutableServiceFactory(new MqttPullExecutableServiceFactory(devicesProviderRegistry, devicesAdapterRegistry), new MqttPushExecutableServiceFactory(devicesAdapterRegistry));
26
+ const mqttFactory = new MqttServiceFactory(mqttFeedExecutableServiceFactory);
27
+ service = mqttFactory.create(configuration);
28
+ break;
29
+ }
30
+ }
31
+ await service.start();
32
+ const shutdown = async () => {
33
+ try {
34
+ logger.info('Shutting down...');
35
+ if (service)
36
+ await service.stop();
37
+ }
38
+ finally {
39
+ process.exit(0);
40
+ }
41
+ };
42
+ logger.info('Application started successfully');
43
+ process.on('SIGINT', () => {
44
+ shutdown().catch((err) => logger.error(err, 'Error during shutdown'));
45
+ });
46
+ process.on('SIGTERM', () => {
47
+ shutdown().catch((err) => logger.error(err, 'Error during shutdown'));
48
+ });
49
+ }
50
+ main().catch((err) => {
51
+ logger.error(err, 'Fatal error occurred');
52
+ process.exit(1);
53
+ });
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Provider that wraps another provider and adapts its output to a different type.
3
+ * Implements the decorator pattern to add transformation capability to any provider.
4
+ *
5
+ * @template T - The input type from the wrapped provider
6
+ * @template K - The output type after adaptation
7
+ */
8
+ export class AdapterProvider {
9
+ /**
10
+ * Creates a new adapter provider.
11
+ * @param provider - The underlying provider that supplies the source data
12
+ * @param adapter - The adapter that transforms the provider's output from type T to type K
13
+ */
14
+ constructor(provider, adapter) {
15
+ this.provider = provider;
16
+ this.adapter = adapter;
17
+ }
18
+ /**
19
+ * Gets data from the provider and adapts it to the target type.
20
+ * @returns A promise that resolves to the adapted data of type K
21
+ */
22
+ async get() {
23
+ const value = await this.provider.get();
24
+ return this.adapter.adapt(value);
25
+ }
26
+ }
27
+ /**
28
+ * Factory for creating AdapterProvider instances.
29
+ * Combines a provider factory with an adapter to create providers with transformation capability.
30
+ *
31
+ * @template Options - The configuration options type for the provider factory
32
+ * @template T - The intermediate type provided by the wrapped provider factory
33
+ * @template K - The final output type after adaptation
34
+ */
35
+ export class AdapterProviderFactory {
36
+ /**
37
+ * Creates a new adapter provider factory.
38
+ * @param providerFactory - Factory to create providers that supply source data
39
+ * @param adapter - Adapter to transform the provider's output
40
+ */
41
+ constructor(providerFactory, adapter) {
42
+ this.providerFactory = providerFactory;
43
+ this.adapter = adapter;
44
+ }
45
+ /**
46
+ * Creates an AdapterProvider instance with the specified options.
47
+ * @param options - Configuration options for the wrapped provider factory
48
+ * @returns An AdapterProvider that supplies adapted data
49
+ */
50
+ create(options) {
51
+ const provider = this.providerFactory.create(options);
52
+ return new AdapterProvider(provider, this.adapter);
53
+ }
54
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Provider that returns a fixed, pre-configured value.
3
+ * Useful for testing, default fallbacks, or static data scenarios.
4
+ * The value can be updated at runtime through the setter.
5
+ *
6
+ * @template T - The type of value this provider supplies
7
+ */
8
+ export class FixedValueProvider {
9
+ /**
10
+ * Gets the current fixed value.
11
+ * @returns The stored value, or undefined if not set
12
+ */
13
+ get value() {
14
+ return this._value;
15
+ }
16
+ /**
17
+ * Sets the fixed value to be returned by this provider.
18
+ * @param value - The value to store and return on subsequent calls
19
+ */
20
+ set value(value) {
21
+ this._value = value;
22
+ }
23
+ /**
24
+ * Gets the fixed value asynchronously.
25
+ * @returns A promise that resolves to the stored value
26
+ */
27
+ get() {
28
+ return Promise.resolve(this._value);
29
+ }
30
+ }
31
+ /**
32
+ * Factory for creating FixedValueProvider instances.
33
+ * Simplifies instantiation of providers with pre-configured static values.
34
+ *
35
+ * @template T - The type of value the created providers will supply
36
+ */
37
+ export class FixedValueProviderFactory {
38
+ /**
39
+ * Creates a new fixed value provider factory.
40
+ * @param properties - Configuration containing the fixed value to provide
41
+ */
42
+ constructor(properties) {
43
+ this.properties = properties;
44
+ }
45
+ /**
46
+ * Creates a FixedValueProvider with the configured value.
47
+ * @returns A FixedValueProvider initialized with the configured value
48
+ */
49
+ create() {
50
+ const provider = new FixedValueProvider();
51
+ provider.value = this.properties.value;
52
+ return provider;
53
+ }
54
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Generic registry for storing and retrieving key-value pairs.
3
+ * Provides a centralized store for managing instances of a specific type.
4
+ *
5
+ * @template T - The type of values stored in this registry
6
+ */
7
+ export class Registry {
8
+ constructor() {
9
+ this.values = new Map();
10
+ }
11
+ /**
12
+ * Registers a value with the given key in the registry.
13
+ * @param key - The unique identifier for the value
14
+ * @param value - The value to store
15
+ */
16
+ register(key, value) {
17
+ this.values.set(key, value);
18
+ }
19
+ /**
20
+ * Retrieves a value from the registry by its key.
21
+ * @param key - The unique identifier of the value to retrieve
22
+ * @returns The stored value, or undefined if the key is not found
23
+ */
24
+ get(key) {
25
+ return this.values.get(key);
26
+ }
27
+ }
@@ -0,0 +1,7 @@
1
+ import { mqttProviderSchema } from './mqtt-configuration.js';
2
+ import { restProviderSchema } from './rest-configuration.js';
3
+ import z from 'zod';
4
+ export const configurationSchema = z.discriminatedUnion('provider', [
5
+ restProviderSchema,
6
+ mqttProviderSchema,
7
+ ]);
@@ -0,0 +1,15 @@
1
+ export const expectationBodySchema = {
2
+ type: 'object',
3
+ properties: {
4
+ voltage: { type: 'number' },
5
+ current: { type: 'number' },
6
+ act_power: { type: 'number' },
7
+ aprt_power: { type: 'number' },
8
+ pf: { type: 'number' },
9
+ freq: { type: 'number' },
10
+ calibration: { type: 'string' },
11
+ errors: { type: 'array', items: { type: 'string' } },
12
+ flags: { type: 'array', items: { type: 'string' } },
13
+ },
14
+ required: ['calibration'],
15
+ };
@@ -0,0 +1,62 @@
1
+ import z from 'zod';
2
+ export const energyInformationSchema = z.object({
3
+ power: z.number(),
4
+ });
5
+ export const mqttPushBridgeFeedSchema = z
6
+ .object({
7
+ url: z.string(),
8
+ topic: z.string(),
9
+ })
10
+ .loose();
11
+ export const mqttPushFeedSchema = z.discriminatedUnion('type', [
12
+ z.object({
13
+ type: z.literal('bridge'),
14
+ properties: mqttPushBridgeFeedSchema,
15
+ }),
16
+ z.object({ type: z.literal('off') }),
17
+ ]);
18
+ export const mqttPullMockFeedSchema = z
19
+ .object({
20
+ interval: z.number().int().nonnegative(),
21
+ value: energyInformationSchema.optional(),
22
+ })
23
+ .loose();
24
+ export const mqttPullAdapterFeedSchema = z
25
+ .object({
26
+ interval: z.number().int().nonnegative(),
27
+ ip: z.string(),
28
+ })
29
+ .loose();
30
+ export const mqttPullFeedSchema = z.discriminatedUnion('type', [
31
+ z.object({
32
+ type: z.literal('adapter'),
33
+ properties: mqttPullAdapterFeedSchema,
34
+ }),
35
+ z.object({
36
+ type: z.literal('mock'),
37
+ properties: mqttPullMockFeedSchema,
38
+ }),
39
+ z.object({ type: z.literal('off') }),
40
+ ]);
41
+ export const mqttFeedModeSchema = z.discriminatedUnion('mode', [
42
+ z.object({
43
+ mode: z.literal('pull'),
44
+ feed: mqttPullFeedSchema,
45
+ }),
46
+ z.object({
47
+ mode: z.literal('push'),
48
+ feed: mqttPushFeedSchema,
49
+ }),
50
+ ]);
51
+ export const mqttMetersSchema = z.object({
52
+ grid: mqttFeedModeSchema,
53
+ solar: mqttFeedModeSchema,
54
+ });
55
+ export const mqttProviderSchema = z.object({
56
+ provider: z.literal('mqtt'),
57
+ properties: z.object({
58
+ url: z.string(),
59
+ device: z.string(),
60
+ meters: mqttMetersSchema,
61
+ }),
62
+ });
@@ -0,0 +1,73 @@
1
+ import z from 'zod';
2
+ // EM1 status (used by mock emulator response)
3
+ export const em1StatusSchema = z.object({
4
+ /**
5
+ * Id of the EM1 component instance
6
+ */
7
+ id: z.number(),
8
+ /**
9
+ * Current measurement value, [A]
10
+ */
11
+ current: z.number().optional(),
12
+ /**
13
+ * Voltage measurement value, [V]
14
+ */
15
+ voltage: z.number().optional(),
16
+ /**
17
+ * Active power measurement value, [W]
18
+ */
19
+ act_power: z.number().optional(),
20
+ /**
21
+ * Apparent power measurement value, [VA] (if applicable)
22
+ */
23
+ aprt_power: z.number().optional(),
24
+ /**
25
+ * Power factor measurement value (if applicable)
26
+ */
27
+ pf: z.number().optional(),
28
+ /**
29
+ * Network frequency measurement value (if applicable)
30
+ */
31
+ freq: z.number().optional(),
32
+ /**
33
+ * Indicates factory calibration or which EM1:id is the source for calibration
34
+ */
35
+ calibration: z.string(),
36
+ /**
37
+ * EM1 component error conditions. May contain power_meter_failure, out_of_range:act_power, out_of_range:aprt_power, out_of_range:voltage, out_of_range:current or ct_type_not_set. Present in status only if not empty.
38
+ */
39
+ errors: z.array(z.string()).optional(),
40
+ /**
41
+ * Communicates present conditions, shown if at least one flag is set. Depending on component capabilites may contain: count_disabled
42
+ */
43
+ flags: z.array(z.string()).optional(),
44
+ });
45
+ export const restAdapterFeedSchema = z.object({ ip: z.string() }).loose();
46
+ export const restMockFeedSchema = z
47
+ .object({ value: em1StatusSchema.optional() })
48
+ .loose();
49
+ export const restFeedSchema = z.object({
50
+ feed: z.discriminatedUnion('type', [
51
+ z.object({
52
+ type: z.literal('adapter'),
53
+ properties: restAdapterFeedSchema,
54
+ }),
55
+ z.object({
56
+ type: z.literal('mock'),
57
+ properties: restMockFeedSchema.optional(),
58
+ }),
59
+ z.object({ type: z.literal('off') }),
60
+ ]),
61
+ });
62
+ export const restMetersSchema = z.object({
63
+ grid: restFeedSchema,
64
+ solar: restFeedSchema,
65
+ });
66
+ export const restProviderSchema = z.object({
67
+ provider: z.literal('rest'),
68
+ properties: z.object({
69
+ port: z.number().int().positive(),
70
+ device: z.string(),
71
+ meters: restMetersSchema,
72
+ }),
73
+ });
@@ -0,0 +1,7 @@
1
+ export const statusQuerySchema = {
2
+ type: 'object',
3
+ properties: {
4
+ id: { type: 'number' },
5
+ },
6
+ required: ['id'],
7
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,55 @@
1
+ import { logger } from '../utils/logger.js';
2
+ import { createMqttClient } from '../utils/mqtt.js';
3
+ /**
4
+ * Service that bridges MQTT messages to a typed callback via an adapter.
5
+ * Subscribes to a topic, parses incoming messages, adapts them, and forwards
6
+ * the adapted payload to the provided callback.
7
+ *
8
+ * @template InputMessage - The raw message type received from MQTT
9
+ * @template Payload - The adapted payload type passed to the callback
10
+ */
11
+ export class MqttBridgeService {
12
+ /**
13
+ * Creates a new MQTT bridge service.
14
+ * @param properties - MQTT connection and subscription details
15
+ * @param callbackProperties - Callback container invoked with adapted payload
16
+ * @param adapter - Adapter that transforms raw MQTT messages to the payload type
17
+ */
18
+ constructor(properties, callbackProperties, adapter) {
19
+ this.properties = properties;
20
+ this.callbackProperties = callbackProperties;
21
+ this.adapter = adapter;
22
+ this.client = null;
23
+ }
24
+ /**
25
+ * Starts the bridge: connects to the broker, subscribes, and wires message handling.
26
+ * @returns A promise that resolves when the subscription is active
27
+ */
28
+ async start() {
29
+ this.client = await createMqttClient(this.properties.url);
30
+ this.client.on('message', (topic, message) => {
31
+ if (topic === this.properties.topic) {
32
+ const data = JSON.parse(message.toString());
33
+ this.adapter
34
+ .adapt(data)
35
+ .then(async (adaptedData) => {
36
+ await this.callbackProperties.callback(adaptedData);
37
+ })
38
+ .catch((error) => {
39
+ logger.error(error, `Error occurred while processing message`);
40
+ });
41
+ }
42
+ });
43
+ await this.client.subscribeAsync(this.properties.topic);
44
+ }
45
+ /**
46
+ * Stops the bridge: disconnects the MQTT client and clears resources.
47
+ * @returns A promise that resolves when the client has disconnected
48
+ */
49
+ async stop() {
50
+ if (this.client) {
51
+ await this.client.endAsync();
52
+ this.client = null;
53
+ }
54
+ }
55
+ }
@@ -0,0 +1,79 @@
1
+ import { logger } from '../utils/logger.js';
2
+ import { createMqttClient } from '../utils/mqtt.js';
3
+ /**
4
+ * MQTT topic for publishing solar power readings.
5
+ */
6
+ const SUN_POWER_TOPIC = 'trydan_v2c_sun_power';
7
+ /**
8
+ * MQTT topic for publishing grid power readings.
9
+ */
10
+ const GRID_POWER_TOPIC = 'trydan_v2c_grid_power';
11
+ /**
12
+ * Service that publishes energy readings to an MQTT broker.
13
+ * Manages MQTT client lifecycle and exposes methods to push grid and solar power values.
14
+ */
15
+ export class MqttService {
16
+ /**
17
+ * Creates a new MQTT service.
18
+ * @param properties - MQTT connection properties including broker URL
19
+ */
20
+ constructor(properties) {
21
+ this.properties = properties;
22
+ this.client = null;
23
+ }
24
+ /**
25
+ * Publishes a grid power reading to the MQTT broker.
26
+ * @param power - Power value in Watts
27
+ */
28
+ async pushGridPower(power) {
29
+ await this.pushReading(power, GRID_POWER_TOPIC);
30
+ }
31
+ /**
32
+ * Publishes a solar power reading to the MQTT broker.
33
+ * @param power - Power value in Watts
34
+ */
35
+ async pushSunPower(power) {
36
+ await this.pushReading(power, SUN_POWER_TOPIC);
37
+ }
38
+ /**
39
+ * Publishes a numeric reading to a specific MQTT topic.
40
+ * @param value - Numeric value to publish
41
+ * @param topic - MQTT topic to publish to
42
+ */
43
+ async pushReading(value, topic) {
44
+ if (!this.client) {
45
+ logger.error('MQTT client not initialized');
46
+ throw new Error('MQTT client not initialized');
47
+ }
48
+ logger.debug({ value, topic }, 'Publishing MQTT message');
49
+ await this.client.publishAsync(topic, String(value));
50
+ }
51
+ /**
52
+ * Starts the MQTT client connection.
53
+ * @returns A promise that resolves when the client is connected
54
+ * @throws {Error} If the client is already started
55
+ */
56
+ async start() {
57
+ logger.info('Starting MQTT mode');
58
+ if (this.client) {
59
+ logger.error('MQTT client already started');
60
+ throw new Error('MQTT client already started');
61
+ }
62
+ this.client = await createMqttClient(this.properties.url);
63
+ }
64
+ /**
65
+ * Stops the MQTT client connection.
66
+ * @returns A promise that resolves when the client is disconnected
67
+ * @throws {Error} If the client is not started
68
+ */
69
+ async stop() {
70
+ const client = this.client;
71
+ this.client = null;
72
+ if (!client) {
73
+ logger.error('MQTT client not started');
74
+ throw new Error('MQTT client not started');
75
+ }
76
+ await client.endAsync();
77
+ logger.info('MQTT client disconnected');
78
+ }
79
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * A no-operation implementation of ExecutableService.
3
+ * Used as a null-object pattern implementation when no actual service execution is needed.
4
+ */
5
+ class NoOpExecutableService {
6
+ /**
7
+ * Starts the service (no-op implementation).
8
+ * @returns An immediately resolved promise
9
+ */
10
+ async start() {
11
+ // no-op
12
+ }
13
+ /**
14
+ * Stops the service (no-op implementation).
15
+ * @returns An immediately resolved promise
16
+ */
17
+ async stop() {
18
+ // no-op
19
+ }
20
+ }
21
+ /**
22
+ * Singleton instance of the no-operation executable service.
23
+ * Use this when a service instance is required but no actual execution is needed.
24
+ */
25
+ export const noOpExecutableService = new NoOpExecutableService();