v2c-any 0.1.4 → 0.1.5

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.
@@ -1,3 +1,4 @@
1
+ import { AbstractExecutableService } from '../service/abstract-executable-service.js';
1
2
  import { MqttService } from '../service/mqtt-service.js';
2
3
  /**
3
4
  * Factory for creating an MQTT service that manages energy publishing.
@@ -45,17 +46,18 @@ export class MqttServiceFactory {
45
46
  energyType: 'solar',
46
47
  callbacks,
47
48
  });
48
- return {
49
- async start() {
49
+ const mqttExecutableService = class extends AbstractExecutableService {
50
+ async doStart() {
50
51
  await mqttService.start();
51
52
  await gridEnergyPublisher.start();
52
53
  await sunEnergyPublisher.start();
53
- },
54
- async stop() {
54
+ }
55
+ async doStop() {
55
56
  await sunEnergyPublisher.stop();
56
57
  await gridEnergyPublisher.stop();
57
58
  await mqttService.stop();
58
- },
59
+ }
59
60
  };
61
+ return new mqttExecutableService();
60
62
  }
61
63
  }
package/dist/index.js CHANGED
@@ -10,29 +10,27 @@ import { MqttPushExecutableServiceFactory } from './factory/mqtt-push-executable
10
10
  import { RestServiceFactory } from './factory/rest-service-factory.js';
11
11
  import { MqttServiceFactory } from './factory/mqtt-service-factory.js';
12
12
  async function main() {
13
- let service = null;
14
13
  await loadDeviceModules();
15
14
  const configurationValidator = new ConfigurationValidator();
16
15
  const configurationLoader = new ConfigurationLoader(configurationValidator);
17
16
  const configuration = await configurationLoader.load();
17
+ let executableServiceFactory;
18
18
  switch (configuration.provider) {
19
19
  case 'rest': {
20
- const emulatorFactory = new RestServiceFactory(new EM1StatusProviderFactory(devicesProviderRegistry));
21
- service = emulatorFactory.create(configuration);
20
+ executableServiceFactory = new RestServiceFactory(new EM1StatusProviderFactory(devicesProviderRegistry));
22
21
  break;
23
22
  }
24
23
  case 'mqtt': {
25
24
  const mqttFeedExecutableServiceFactory = new MqttFeedExecutableServiceFactory(new MqttPullExecutableServiceFactory(devicesProviderRegistry, devicesAdapterRegistry), new MqttPushExecutableServiceFactory(devicesAdapterRegistry));
26
- const mqttFactory = new MqttServiceFactory(mqttFeedExecutableServiceFactory);
27
- service = mqttFactory.create(configuration);
25
+ executableServiceFactory = new MqttServiceFactory(mqttFeedExecutableServiceFactory);
28
26
  break;
29
27
  }
30
28
  }
29
+ const service = executableServiceFactory.create(configuration);
31
30
  const shutdown = async () => {
32
31
  try {
33
32
  logger.info('Shutting down...');
34
- if (service)
35
- await service.stop();
33
+ await service?.stop();
36
34
  logger.info('Shutdown complete');
37
35
  }
38
36
  catch (err) {
@@ -0,0 +1,48 @@
1
+ import { logger } from '../utils/logger.js';
2
+ /**
3
+ * Abstract base class for executable services.
4
+ * Provides idempotent start/stop lifecycle management with state tracking.
5
+ * Subclasses implement the actual service logic via doStart() and doStop() hooks.
6
+ */
7
+ export class AbstractExecutableService {
8
+ constructor() {
9
+ /**
10
+ * Internal state flag tracking whether the service is currently started.
11
+ * @private
12
+ */
13
+ this._started = false;
14
+ }
15
+ /**
16
+ * Returns whether the service is currently started.
17
+ * @returns true if the service has been started and not yet stopped
18
+ */
19
+ get started() {
20
+ return this._started;
21
+ }
22
+ /**
23
+ * Starts the service.
24
+ * Idempotent: calling start() on an already-started service is a no-op with a warning.
25
+ * @returns A promise that resolves when the service has successfully started
26
+ */
27
+ async start() {
28
+ if (this._started) {
29
+ logger.warn(`Service is already started`);
30
+ return;
31
+ }
32
+ await this.doStart();
33
+ this._started = true;
34
+ }
35
+ /**
36
+ * Stops the service.
37
+ * Idempotent: calling stop() on an already-stopped service is a no-op with a warning.
38
+ * @returns A promise that resolves when the service has successfully stopped
39
+ */
40
+ async stop() {
41
+ if (!this._started) {
42
+ logger.warn(`Service is not started`);
43
+ return;
44
+ }
45
+ await this.doStop();
46
+ this._started = false;
47
+ }
48
+ }
@@ -1,5 +1,6 @@
1
1
  import { logger } from '../utils/logger.js';
2
2
  import { createMqttClient } from '../utils/mqtt.js';
3
+ import { AbstractExecutableService } from './abstract-executable-service.js';
3
4
  /**
4
5
  * Service that bridges MQTT messages to a typed callback via an adapter.
5
6
  * Subscribes to a topic, parses incoming messages, adapts them, and forwards
@@ -8,7 +9,7 @@ import { createMqttClient } from '../utils/mqtt.js';
8
9
  * @template InputMessage - The raw message type received from MQTT
9
10
  * @template Payload - The adapted payload type passed to the callback
10
11
  */
11
- export class MqttBridgeService {
12
+ export class MqttBridgeService extends AbstractExecutableService {
12
13
  /**
13
14
  * Creates a new MQTT bridge service.
14
15
  * @param properties - MQTT connection and subscription details
@@ -16,6 +17,7 @@ export class MqttBridgeService {
16
17
  * @param adapter - Adapter that transforms raw MQTT messages to the payload type
17
18
  */
18
19
  constructor(properties, callbackProperties, adapter) {
20
+ super();
19
21
  this.properties = properties;
20
22
  this.callbackProperties = callbackProperties;
21
23
  this.adapter = adapter;
@@ -25,7 +27,7 @@ export class MqttBridgeService {
25
27
  * Starts the bridge: connects to the broker, subscribes, and wires message handling.
26
28
  * @returns A promise that resolves when the subscription is active
27
29
  */
28
- async start() {
30
+ async doStart() {
29
31
  logger.info('Starting MQTT bridge service');
30
32
  this.client = await createMqttClient(this.properties.url);
31
33
  this.client.on('message', (topic, message) => {
@@ -49,12 +51,10 @@ export class MqttBridgeService {
49
51
  * Stops the bridge: disconnects the MQTT client and clears resources.
50
52
  * @returns A promise that resolves when the client has disconnected
51
53
  */
52
- async stop() {
54
+ async doStop() {
53
55
  logger.info('Stopping MQTT bridge service');
54
- if (this.client) {
55
- await this.client.endAsync();
56
- this.client = null;
57
- }
56
+ await this.client?.endAsync();
57
+ this.client = null;
58
58
  logger.info('MQTT bridge service stopped');
59
59
  }
60
60
  }
@@ -1,5 +1,6 @@
1
1
  import { logger } from '../utils/logger.js';
2
2
  import { createMqttClient } from '../utils/mqtt.js';
3
+ import { AbstractExecutableService } from './abstract-executable-service.js';
3
4
  /**
4
5
  * MQTT topic for publishing solar power readings.
5
6
  */
@@ -12,12 +13,13 @@ const GRID_POWER_TOPIC = 'trydan_v2c_grid_power';
12
13
  * Service that publishes energy readings to an MQTT broker.
13
14
  * Manages MQTT client lifecycle and exposes methods to push grid and solar power values.
14
15
  */
15
- export class MqttService {
16
+ export class MqttService extends AbstractExecutableService {
16
17
  /**
17
18
  * Creates a new MQTT service.
18
19
  * @param properties - MQTT connection properties including broker URL
19
20
  */
20
21
  constructor(properties) {
22
+ super();
21
23
  this.properties = properties;
22
24
  this.client = null;
23
25
  }
@@ -53,12 +55,8 @@ export class MqttService {
53
55
  * @returns A promise that resolves when the client is connected
54
56
  * @throws {Error} If the client is already started
55
57
  */
56
- async start() {
58
+ async doStart() {
57
59
  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
60
  this.client = await createMqttClient(this.properties.url);
63
61
  logger.info('MQTT client started');
64
62
  }
@@ -67,15 +65,10 @@ export class MqttService {
67
65
  * @returns A promise that resolves when the client is disconnected
68
66
  * @throws {Error} If the client is not started
69
67
  */
70
- async stop() {
68
+ async doStop() {
71
69
  logger.info('Stopping MQTT mode');
72
- const client = this.client;
70
+ await this.client?.endAsync();
73
71
  this.client = null;
74
- if (!client) {
75
- logger.error('MQTT client not started');
76
- throw new Error('MQTT client not started');
77
- }
78
- await client.endAsync();
79
72
  logger.info('MQTT client disconnected');
80
73
  }
81
74
  }
@@ -1,20 +1,21 @@
1
+ import { AbstractExecutableService } from './abstract-executable-service.js';
1
2
  /**
2
3
  * A no-operation implementation of ExecutableService.
3
4
  * Used as a null-object pattern implementation when no actual service execution is needed.
4
5
  */
5
- class NoOpExecutableService {
6
+ class NoOpExecutableService extends AbstractExecutableService {
6
7
  /**
7
8
  * Starts the service (no-op implementation).
8
9
  * @returns An immediately resolved promise
9
10
  */
10
- async start() {
11
+ async doStart() {
11
12
  // no-op
12
13
  }
13
14
  /**
14
15
  * Stops the service (no-op implementation).
15
16
  * @returns An immediately resolved promise
16
17
  */
17
- async stop() {
18
+ async doStop() {
18
19
  // no-op
19
20
  }
20
21
  }
@@ -1,4 +1,5 @@
1
1
  import { logger } from '../utils/logger.js';
2
+ import { AbstractExecutableService } from './abstract-executable-service.js';
2
3
  /**
3
4
  * Periodic pull-then-push service.
4
5
  * Fetches data from a `Provider` at a fixed interval and forwards it via a callback.
@@ -6,7 +7,7 @@ import { logger } from '../utils/logger.js';
6
7
  *
7
8
  * @template Payload - The type of data provided and pushed
8
9
  */
9
- export class PullPushService {
10
+ export class PullPushService extends AbstractExecutableService {
10
11
  /**
11
12
  * Creates a new pull/push service.
12
13
  * @param provider - Source `Provider` that supplies data
@@ -14,6 +15,7 @@ export class PullPushService {
14
15
  * @param callbackProperties - Callback container invoked with fetched data
15
16
  */
16
17
  constructor(provider, intervalMs, callbackProperties) {
18
+ super();
17
19
  this.provider = provider;
18
20
  this.intervalMs = intervalMs;
19
21
  this.callbackProperties = callbackProperties;
@@ -24,7 +26,7 @@ export class PullPushService {
24
26
  * @returns A promise that resolves once the service starts
25
27
  * @throws {Error} If the service is already started
26
28
  */
27
- async start() {
29
+ doStart() {
28
30
  if (this.abortController) {
29
31
  throw new Error('Adapter already started');
30
32
  }
@@ -37,7 +39,7 @@ export class PullPushService {
37
39
  * Stops periodic polling if running.
38
40
  * @returns A promise that resolves once the service stops
39
41
  */
40
- stop() {
42
+ doStop() {
41
43
  this.abortController?.abort();
42
44
  this.abortController = null;
43
45
  return Promise.resolve();
@@ -4,13 +4,15 @@ import { expectationBodySchema } from '../schema/expectation-body.js';
4
4
  import { statusQuerySchema } from '../schema/status-query.js';
5
5
  import { logger } from '../utils/logger.js';
6
6
  import { FixedValueProvider } from '../provider/fixed-value-provider.js';
7
+ import { AbstractExecutableService } from './abstract-executable-service.js';
7
8
  /**
8
9
  * REST service that exposes Shelly EM1-like endpoints for energy status.
9
10
  * Provides health checks, mock expectation updates, and status queries for grid and solar.
10
11
  * Implements the executable service lifecycle to start and stop the HTTP server.
11
12
  */
12
- export class RestService {
13
+ export class RestService extends AbstractExecutableService {
13
14
  constructor(gridEnergyProvider, solarEnergyProvider, properties) {
15
+ super();
14
16
  this.gridEnergyProvider = gridEnergyProvider;
15
17
  this.solarEnergyProvider = solarEnergyProvider;
16
18
  this.properties = properties;
@@ -41,7 +43,8 @@ export class RestService {
41
43
  * - `GET /rpc/EM1.GetStatus` fetch status for a given id
42
44
  * @returns A promise that resolves when the server is listening
43
45
  */
44
- async start() {
46
+ async doStart() {
47
+ logger.info('Starting REST service...');
45
48
  const app = Fastify({
46
49
  loggerInstance: logger,
47
50
  disableRequestLogging: true,
@@ -92,16 +95,17 @@ export class RestService {
92
95
  //app.post('/rpc/EM1.GetStatus', async () => app.inject({ method: 'GET', url: '/rpc/EM1.GetStatus' }).then(r => r.json()));
93
96
  await app.listen({ port: this.properties.port, host: '0.0.0.0' });
94
97
  logger.info({ port: this.properties.port }, 'Listening');
98
+ logger.info('REST service started');
95
99
  }
96
100
  /**
97
101
  * Stops the REST server if running.
98
102
  * @returns A promise that resolves when the server has closed
99
103
  */
100
- async stop() {
101
- logger.info('Stopping emulator...');
104
+ async doStop() {
105
+ logger.info('Stopping REST service...');
102
106
  if (this.app) {
103
107
  await this.app.close();
104
108
  }
105
- return Promise.resolve();
109
+ logger.info('REST service stopped');
106
110
  }
107
111
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "v2c-any",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "type": "module",
5
5
  "description": "V2C device adapter and MQTT publisher (Shelly EM1 compatible)",
6
6
  "main": "dist/index.js",