mirror of
				https://github.com/bourquep/mysa2mqtt.git
				synced 2025-11-03 21:29:41 +00:00 
			
		
		
		
	build: Build and publish a proper CLI tool with options, also packaged as a Docker image (#2)
* Renamed environment variables * Moved MqttSettings to main.tsx * Using Commander for CLI arguments * PinoLogger * Option for json log format * Updated mysa-js-sdk to latest version * Moved options to their own module * Extracted session file management to the session module * Added deviceId meta to thermostat instance logger * Display version from package.json; added copyright * Create README.md * Build with tsup * Update .gitignore * Remove prepublishOnly npm script * Distributed CLI executable is now working * Update README.md * Dockerfile * Minify the build output * Update README.md * Create initial Github workflow * Create release.config.mjs * Read package version at run-time, not build-time * Update README.md * Create CONTRIBUTING.md * WIP: docker CI job * Trying multiple tags * Enable docker build cache * Testing the docker build cache * Dockerfile: set npm version in final stage for better caching * Testing docker build cache * Moved VERSION arg to the final build stage * Finalized the `docker` build job * Added copyright header to all source files * Specify radix when parsing integer options
This commit is contained in:
		@@ -1,37 +1,51 @@
 | 
			
		||||
/*
 | 
			
		||||
mysa2mqtt
 | 
			
		||||
Copyright (C) 2025 Pascal Bourque
 | 
			
		||||
 | 
			
		||||
Permission is hereby granted, free of charge, to any person obtaining a copy
 | 
			
		||||
of this software and associated documentation files (the "Software"), to deal
 | 
			
		||||
in the Software without restriction, including without limitation the rights
 | 
			
		||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 | 
			
		||||
copies of the Software, and to permit persons to whom the Software is
 | 
			
		||||
furnished to do so, subject to the following conditions:
 | 
			
		||||
 | 
			
		||||
The above copyright notice and this permission notice shall be included in all
 | 
			
		||||
copies or substantial portions of the Software.
 | 
			
		||||
 | 
			
		||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 | 
			
		||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 | 
			
		||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 | 
			
		||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 | 
			
		||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 | 
			
		||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 | 
			
		||||
SOFTWARE.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
import { Climate, ClimateAction, DeviceConfiguration, Logger, MqttSettings, Sensor } from 'mqtt2ha';
 | 
			
		||||
import { DeviceBase, FirmwareDevice, MysaApiClient, MysaDeviceMode, StateChange, Status } from 'mysa-js-sdk';
 | 
			
		||||
 | 
			
		||||
export class Thermostat {
 | 
			
		||||
  private isStarted = false;
 | 
			
		||||
  private mqttSettings: MqttSettings;
 | 
			
		||||
  private mqttDevice: DeviceConfiguration;
 | 
			
		||||
  private mqttClimate: Climate;
 | 
			
		||||
  private mqttPower: Sensor;
 | 
			
		||||
  private readonly mqttDevice: DeviceConfiguration;
 | 
			
		||||
  private readonly mqttClimate: Climate;
 | 
			
		||||
  private readonly mqttPower: Sensor;
 | 
			
		||||
 | 
			
		||||
  private readonly mysaStatusUpdateHandler = this.handleMysaStatusUpdate.bind(this);
 | 
			
		||||
  private readonly mysaStateChangeHandler = this.handleMysaStateChange.bind(this);
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    public client: MysaApiClient,
 | 
			
		||||
    public device: DeviceBase,
 | 
			
		||||
    private logger: Logger,
 | 
			
		||||
    public deviceFirmware?: FirmwareDevice
 | 
			
		||||
    public readonly mysaApiClient: MysaApiClient,
 | 
			
		||||
    public readonly mysaDevice: DeviceBase,
 | 
			
		||||
    private readonly mqttSettings: MqttSettings,
 | 
			
		||||
    private readonly logger: Logger,
 | 
			
		||||
    public readonly mysaDeviceFirmware?: FirmwareDevice
 | 
			
		||||
  ) {
 | 
			
		||||
    this.mqttSettings = {
 | 
			
		||||
      host: process.env.MYSA_2_MQTT_BROKER_HOST || 'localhost',
 | 
			
		||||
      port: parseInt(process.env.MYSA_2_MQTT_BROKER_PORT || '1883'),
 | 
			
		||||
      username: process.env.MYSA_2_MQTT_BROKER_USERNAME,
 | 
			
		||||
      password: process.env.MYSA_2_MQTT_BROKER_PASSWORD,
 | 
			
		||||
      client_name: 'mysa2mqtt',
 | 
			
		||||
      state_prefix: 'mysa2mqtt'
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    this.mqttDevice = {
 | 
			
		||||
      identifiers: device.Id,
 | 
			
		||||
      name: device.Name,
 | 
			
		||||
      identifiers: mysaDevice.Id,
 | 
			
		||||
      name: mysaDevice.Name,
 | 
			
		||||
      manufacturer: 'Mysa',
 | 
			
		||||
      model: device.Model,
 | 
			
		||||
      sw_version: deviceFirmware?.InstalledVersion
 | 
			
		||||
      model: mysaDevice.Model,
 | 
			
		||||
      sw_version: mysaDeviceFirmware?.InstalledVersion
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    this.mqttClimate = new Climate(
 | 
			
		||||
@@ -41,10 +55,10 @@ export class Thermostat {
 | 
			
		||||
        component: {
 | 
			
		||||
          component: 'climate',
 | 
			
		||||
          device: this.mqttDevice,
 | 
			
		||||
          unique_id: `mysa_${device.Id}_climate`,
 | 
			
		||||
          unique_id: `mysa_${mysaDevice.Id}_climate`,
 | 
			
		||||
          name: 'Thermostat',
 | 
			
		||||
          min_temp: device.MinSetpoint,
 | 
			
		||||
          max_temp: device.MaxSetpoint,
 | 
			
		||||
          min_temp: mysaDevice.MinSetpoint,
 | 
			
		||||
          max_temp: mysaDevice.MaxSetpoint,
 | 
			
		||||
          modes: ['off', 'heat'], // TODO: AC
 | 
			
		||||
          precision: 0.1,
 | 
			
		||||
          temp_step: 0.5,
 | 
			
		||||
@@ -64,16 +78,16 @@ export class Thermostat {
 | 
			
		||||
      async (topic, message) => {
 | 
			
		||||
        switch (topic) {
 | 
			
		||||
          case 'mode_command_topic':
 | 
			
		||||
            this.client.setDeviceState(
 | 
			
		||||
              this.device.Id,
 | 
			
		||||
            this.mysaApiClient.setDeviceState(
 | 
			
		||||
              this.mysaDevice.Id,
 | 
			
		||||
              undefined,
 | 
			
		||||
              message === 'off' ? 'off' : message === 'heat' ? 'heat' : undefined
 | 
			
		||||
            );
 | 
			
		||||
            break;
 | 
			
		||||
 | 
			
		||||
          case 'power_command_topic':
 | 
			
		||||
            this.client.setDeviceState(
 | 
			
		||||
              this.device.Id,
 | 
			
		||||
            this.mysaApiClient.setDeviceState(
 | 
			
		||||
              this.mysaDevice.Id,
 | 
			
		||||
              undefined,
 | 
			
		||||
              message === 'OFF' ? 'off' : message === 'ON' ? 'heat' : undefined
 | 
			
		||||
            );
 | 
			
		||||
@@ -81,9 +95,9 @@ export class Thermostat {
 | 
			
		||||
 | 
			
		||||
          case 'temperature_command_topic':
 | 
			
		||||
            if (message === '') {
 | 
			
		||||
              this.client.setDeviceState(this.device.Id, undefined, undefined);
 | 
			
		||||
              this.mysaApiClient.setDeviceState(this.mysaDevice.Id, undefined, undefined);
 | 
			
		||||
            } else {
 | 
			
		||||
              this.client.setDeviceState(this.device.Id, parseFloat(message), undefined);
 | 
			
		||||
              this.mysaApiClient.setDeviceState(this.mysaDevice.Id, parseFloat(message), undefined);
 | 
			
		||||
            }
 | 
			
		||||
            break;
 | 
			
		||||
        }
 | 
			
		||||
@@ -96,7 +110,7 @@ export class Thermostat {
 | 
			
		||||
      component: {
 | 
			
		||||
        component: 'sensor',
 | 
			
		||||
        device: this.mqttDevice,
 | 
			
		||||
        unique_id: `mysa_${device.Id}_power`,
 | 
			
		||||
        unique_id: `mysa_${mysaDevice.Id}_power`,
 | 
			
		||||
        device_class: 'power',
 | 
			
		||||
        state_class: 'measurement',
 | 
			
		||||
        unit_of_measurement: 'W',
 | 
			
		||||
@@ -114,8 +128,8 @@ export class Thermostat {
 | 
			
		||||
    this.isStarted = true;
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      const deviceStates = await this.client.getDeviceStates();
 | 
			
		||||
      const state = deviceStates.DeviceStatesObj[this.device.Id];
 | 
			
		||||
      const deviceStates = await this.mysaApiClient.getDeviceStates();
 | 
			
		||||
      const state = deviceStates.DeviceStatesObj[this.mysaDevice.Id];
 | 
			
		||||
 | 
			
		||||
      this.mqttClimate.currentTemperature = state.CorrectedTemp.v;
 | 
			
		||||
      this.mqttClimate.currentHumidity = state.Humidity.v;
 | 
			
		||||
@@ -129,10 +143,10 @@ export class Thermostat {
 | 
			
		||||
      await this.mqttPower.setState('state_topic', 'None');
 | 
			
		||||
      await this.mqttPower.writeConfig();
 | 
			
		||||
 | 
			
		||||
      this.client.emitter.on('statusChanged', this.mysaStatusUpdateHandler);
 | 
			
		||||
      this.client.emitter.on('stateChanged', this.mysaStateChangeHandler);
 | 
			
		||||
      this.mysaApiClient.emitter.on('statusChanged', this.mysaStatusUpdateHandler);
 | 
			
		||||
      this.mysaApiClient.emitter.on('stateChanged', this.mysaStateChangeHandler);
 | 
			
		||||
 | 
			
		||||
      await this.client.startRealtimeUpdates(this.device.Id);
 | 
			
		||||
      await this.mysaApiClient.startRealtimeUpdates(this.mysaDevice.Id);
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      this.isStarted = false;
 | 
			
		||||
      throw error;
 | 
			
		||||
@@ -146,16 +160,16 @@ export class Thermostat {
 | 
			
		||||
 | 
			
		||||
    this.isStarted = false;
 | 
			
		||||
 | 
			
		||||
    await this.client.stopRealtimeUpdates(this.device.Id);
 | 
			
		||||
    await this.mysaApiClient.stopRealtimeUpdates(this.mysaDevice.Id);
 | 
			
		||||
 | 
			
		||||
    this.client.emitter.off('statusChanged', this.mysaStatusUpdateHandler);
 | 
			
		||||
    this.client.emitter.off('stateChanged', this.mysaStateChangeHandler);
 | 
			
		||||
    this.mysaApiClient.emitter.off('statusChanged', this.mysaStatusUpdateHandler);
 | 
			
		||||
    this.mysaApiClient.emitter.off('stateChanged', this.mysaStateChangeHandler);
 | 
			
		||||
 | 
			
		||||
    await this.mqttPower.setState('state_topic', 'None');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async handleMysaStatusUpdate(status: Status) {
 | 
			
		||||
    if (!this.isStarted || status.deviceId !== this.device.Id) {
 | 
			
		||||
    if (!this.isStarted || status.deviceId !== this.mysaDevice.Id) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -165,7 +179,7 @@ export class Thermostat {
 | 
			
		||||
    this.mqttClimate.targetTemperature = this.mqttClimate.currentMode !== 'off' ? status.setPoint : undefined;
 | 
			
		||||
 | 
			
		||||
    if (status.current != null) {
 | 
			
		||||
      const watts = this.device.Voltage * status.current;
 | 
			
		||||
      const watts = this.mysaDevice.Voltage * status.current;
 | 
			
		||||
      await this.mqttPower.setState('state_topic', watts.toFixed(2));
 | 
			
		||||
    } else {
 | 
			
		||||
      await this.mqttPower.setState('state_topic', 'None');
 | 
			
		||||
@@ -173,7 +187,7 @@ export class Thermostat {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async handleMysaStateChange(state: StateChange) {
 | 
			
		||||
    if (!this.isStarted || state.deviceId !== this.device.Id) {
 | 
			
		||||
    if (!this.isStarted || state.deviceId !== this.mysaDevice.Id) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user