Skip to content

Commit

Permalink
better mqtt (#9)
Browse files Browse the repository at this point in the history
* better mqtt; package updates; some improvements

* larger refresh interval
  • Loading branch information
ronnyf authored Jul 28, 2024
1 parent 541a59a commit b228ee2
Show file tree
Hide file tree
Showing 10 changed files with 970 additions and 825 deletions.
10 changes: 8 additions & 2 deletions config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
"title": "Refresh Interval",
"type": "number",
"description": "Number of seconds between refreshes of the battery data.",
"default": 10
"default": 30
},
"mqttClientID": {
"title": "Client ID",
Expand All @@ -51,14 +51,20 @@
"title": "MQTT User Name",
"type": "string",
"description": "Your mqtt user.",
"placeholder": "John Appleseed",
"required": false
},
"mqttPassword": {
"title": "Password",
"type": "string",
"description": "Your mqtt password.",
"required": false
},
"mqttRootTopic": {
"title": "MQTT topic for Sonnen updates.",
"type": "string",
"description": "Your root topic (default = Sonnen).",
"placeholder": "Sonnen",
"required": true
}
}
}
Expand Down
1,412 changes: 781 additions & 631 deletions package-lock.json

Large diffs are not rendered by default.

12 changes: 6 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"private": false,
"displayName": "Homebridge SonnenBatterie V2",
"name": "homebridge-sonnenbatterie-v2",
"version": "1.0.2",
"version": "1.0.3",
"description": "A Homebridge plugin for a SonnenBatterie with v2 firmware.",
"license": "Apache-2.0",
"repository": {
Expand Down Expand Up @@ -32,18 +32,18 @@
],
"dependencies": {
"axios": "^1.6.5",
"mqtt": "^4.3.7"
"mqtt": "^5.8.1"
},
"devDependencies": {
"@types/node": "^20.10.0",
"@typescript-eslint/eslint-plugin": "^7.2.0",
"@typescript-eslint/parser": "^7.2.0",
"eslint": "^8.45.0",
"homebridge": "^1.6.0",
"eslint": "^8.57.0",
"homebridge": "^1.7.0",
"nodemon": "^2.0.22",
"prettier": "3.2.5",
"prettier": "^3.2.5",
"rimraf": "^3.0.2",
"ts-node": "^10.9.2",
"typescript": "^4.9.5"
"typescript": "^5.5.4"
}
}
114 changes: 114 additions & 0 deletions src/garageclient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { Logger } from 'homebridge';
import mqtt, { IClientOptions, IClientPublishOptions, IClientSubscribeOptions, ISubscriptionGrant, MqttClient, OnMessageCallback } from 'mqtt';

export class SonnenMQTT {

private client: MqttClient | null;
private rootTopic: string;

constructor(client: MqttClient | null, rootTopic: string) {
this.client = client;
this.rootTopic = rootTopic;
}

connect(clientID: string, username: string, password: string, host: string, log: Logger | null = null) {
log?.debug("connecting... (user: ", username, "), password: (****))");
const opts: IClientOptions = {
clientId: clientID,
rejectUnauthorized: true,
clean: true,
username: username,
password: password,
};
this.client = mqtt.connect(host, opts);
log?.debug("did connect, client...");
}

async connectAsync(clientID: string, username: string, password: string, host: string, log: Logger | null = null): Promise<void> {
log?.debug("connecting async... (user: ", username, "), password: (****)");
const opts: IClientOptions = {
clientId: clientID,
rejectUnauthorized: true,
clean: true,
username: username,
password: password,
};
this.client = await mqtt.connectAsync(host, opts);
log?.debug("did connect, client...");
}

reconnect(): boolean {
if (this.client === null) { return false; }
if (this.client.connected == true) {
return true
}
this.client = this.client.reconnect()
return this.client !== null;
}

getConnected(): boolean {
if (this.client === null) { return false; }
return this.client.connected;
}

getClient(): MqttClient | null {
return this.client;
}

async addSubscription(topic: string | string[]): Promise<ISubscriptionGrant[] | null> {
const opts: IClientSubscribeOptions = { qos: 1 };
// qos: 1 means we want the message to arrive at least once but don't care if it arrives twice (or more
if (this.client === null) {
return null;
}
return this.client.subscribeAsync(topic, opts);
}

onMessage(callback: OnMessageCallback) {
if (this.client === null) { return false; }
this.client.on('message', callback);
}

publish<V>(topic: string, value: V, log: Logger | null) {
log?.debug("pulishing ", value, " to topic ", topic);
const stringValue = String(value);
if (stringValue === undefined) {
log?.error("cannot convert value ", value ," to string");
return;
}

if (this.client === null) {
log?.error("client is null");
const success = this.reconnect();
if (success == false) {
log?.error("reconnect failed");
}
}

const opts: IClientPublishOptions = {
qos: 1,
retain: false,
properties: {
messageExpiryInterval: 30
}
};

const topicValue = this.rootTopic.concat("/").concat(topic);
log?.debug("pulishing topic: ", topicValue, " << ", stringValue);

if (this.client != null) {
if (this.client.connected == false) {
log?.error("client is not connected");
}

log?.debug("publishing value: ", stringValue);
this.client.publish(topicValue, stringValue, opts);
}
}

async disconnect() {
if (this.client === null) { return false; }
return this.client.endAsync;
}

}
94 changes: 42 additions & 52 deletions src/platform.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
// eslint-disable-next-line max-len
import {
API,
DynamicPlatformPlugin,
Expand All @@ -14,7 +13,7 @@ import {
SonnenAccessoryFactory,
UpdatableAccessory,
} from "./sonnenAccessory";
import { SonnenMQTT } from "./sonnenMQTT";
import { SonnenMQTT } from "./garageclient";

/**
* HomebridgePlatform
Expand All @@ -28,7 +27,7 @@ export class SonnenHomebridgePlatform implements DynamicPlatformPlugin {
// this is used to track restored cached accessories
public readonly accessories: PlatformAccessory[] = [];
public readonly sonnenAPI: SonnenAPI;
public readonly sonnenMQTT: SonnenMQTT;
public readonly sonnenMqtt: SonnenMQTT;
private updatableAccessories: UpdatableAccessory[] = [];

constructor(
Expand All @@ -38,9 +37,12 @@ export class SonnenHomebridgePlatform implements DynamicPlatformPlugin {
) {
this.Service = api.hap.Service;
this.Characteristic = api.hap.Characteristic;

this.log.debug("Finished initializing platform:", config.name);

this.sonnenAPI = new SonnenAPI(2, 1, config, log);
this.sonnenMQTT = new SonnenMQTT(config, log);
const mqttRootTopic = this.config['mqttRootTopic'] ?? 'Sonnen';
this.sonnenMqtt = new SonnenMQTT(null, mqttRootTopic);

// When this event is fired it means Homebridge has restored all cached accessories from disk.
// Dynamic Platform plugins should only register new accessories after this event was fired,
Expand All @@ -50,6 +52,9 @@ export class SonnenHomebridgePlatform implements DynamicPlatformPlugin {
log.debug("discovering devices");
// run the method to discover / register your devices as accessories
this.discoverDevices();
this.mqttConnect();
this.registerAccessories()
this.runloop();
});
}

Expand All @@ -70,7 +75,7 @@ export class SonnenHomebridgePlatform implements DynamicPlatformPlugin {
* must not be registered again to prevent "duplicate UUID" errors.
*/

async discoverDevices() {
async discoverDevices(): Promise<void> {
this.log.info("discovering SonnenBatterie");

const config = await this.sonnenAPI.fetchConfiguration();
Expand All @@ -80,29 +85,46 @@ export class SonnenHomebridgePlatform implements DynamicPlatformPlugin {
this.log.info(`SW: ${config.DE_Software}`);

await this.sonnenAPI.reloadBatteryStatus();
this.log.debug(`status: ${JSON.stringify(this.sonnenAPI.batteryStatus)}`);
this.log.debug(`1st status: ${JSON.stringify(this.sonnenAPI.batteryStatus)}`);
}

const factory = new SonnenAccessoryFactory(
this,
this.api,
this.sonnenMQTT,
this.log,
);
async mqttConnect(): Promise<void> {
this.log.info("connecting to mqtt broker");

// TODO: enumerate something to make this less hard-coded
this.registerAccessory(factory, AccessoryType.Production);
this.registerAccessory(factory, AccessoryType.Consumption);
this.registerAccessory(factory, AccessoryType.Grid);
const clientID = this.config['mqttClientID'] ?? 'SonnenMQTT';
const username = this.config['mqttUser'];
const password = this.config['mqttPassword'];

if (username == null) {
this.log.warn("username is null")
}
if (password == null) {
this.log.warn("password is null")
}

const host = this.config['mqttHost'] ?? 'mqtt://localhost:1883';

const interval: number = this.config["refreshInterval"] ?? 10;
await this.sonnenMqtt.connectAsync(clientID, username, password, host);
this.log.debug("did connect to mqtt broker");
}

runloop() {
const interval: number = this.config["refreshInterval"] ?? 30;
setInterval(() => {
try {
this.fetchSonnenStatus();
} catch (error) {
this.log.error(`Error fetching latestData from SonnenAPI: ${error}`);
}
}, interval * 100);
}, interval * 1000);
}

registerAccessories() {
const factory = new SonnenAccessoryFactory(this);

this.registerAccessory(factory, AccessoryType.Production);
this.registerAccessory(factory, AccessoryType.Consumption);
this.registerAccessory(factory, AccessoryType.Grid);
}

async registerAccessory(
Expand Down Expand Up @@ -137,44 +159,12 @@ export class SonnenHomebridgePlatform implements DynamicPlatformPlugin {

async fetchSonnenStatus() {
await this.sonnenAPI.reloadBatteryStatus();
// this.log.info(`did fetch battery status: ${JSON.stringify(this.sonnenAPI.batteryStatus)}`);
// this.log.debug(`did fetch battery status: ${JSON.stringify(this.sonnenAPI.batteryStatus)}`);

await this.sonnenAPI.reloadInverterStatus();
// this.log.info(`did fetch inverter status: ${JSON.stringify(this.sonnenAPI.inverterStatus)}`);
// this.log.debug(`did fetch inverter status: ${JSON.stringify(this.sonnenAPI.inverterStatus)}`);

// updating post fetch
this.updateAccessories();
}

async getStatusValue<T>(value: T): Promise<T> {
this.log.debug("returning status value ->", value);
return value;
}

// battery status bindables
// async getBatteryLevel(): Promise<CharacteristicValue> {
// const battery = this.batteryStatus.Level;
// this.log.debug('Get Characteristic BatteryLevel ->', battery);
// return battery;
// }

// async getBatteryLowChargeState(): Promise<CharacteristicValue> {
// const lowBatteryStatus = this.batteryStatus.Level > this.batteryStatus.Backup
// ? this.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL
// : this.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW;
// this.log.debug('Get Characteristic LowBattery ->', lowBatteryStatus);
// return lowBatteryStatus;
// }

// async getProduction(): Promise<CharacteristicValue> {
// const production = this.batteryStatus.Production;
// this.log.debug('Get Characteristic Production ->', production);
// return production;
// }

// async getProductionOn(): Promise<CharacteristicValue> {
// const productionOn = this.batteryStatus.Production > 0 ?? false;
// this.log.debug('Get Characteristic ProductionOn ->', productionOn);
// return productionOn;
// }
}
9 changes: 3 additions & 6 deletions src/sonnenAccessory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { PLATFORM_NAME, PLUGIN_NAME } from './settings';
import { SonnenHomebridgePlatform } from './platform';
import { SonnenBatterieProductionAccessory } from './sonnenProductionAccessory';
import { BatteryStatus, InverterStatus } from './sonnenApi';
import { SonnenMQTT } from './sonnenMQTT';
import { SonnenBatterieConsumptionAccessory } from './sonnenConsumptionAccessory';
import { SonnenBatterieGridAccessory } from './sonnenGridAccessory';

Expand All @@ -21,14 +20,12 @@ export class SonnenAccessoryFactory {

private platform: SonnenHomebridgePlatform;
private api: API;
private mqtt: SonnenMQTT;
private log: Logger;

constructor(platform: SonnenHomebridgePlatform, api: API, mqtt: SonnenMQTT, log: Logger) {
constructor(platform: SonnenHomebridgePlatform) {
this.platform = platform;
this.api = api;
this.mqtt = mqtt
this.log = log;
this.api = platform.api;
this.log = platform.log;
}

makePlugin(displayName: string, uuid: string): UpdatableAccessory | null {
Expand Down
8 changes: 4 additions & 4 deletions src/sonnenConsumptionAccessory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,15 +92,15 @@ export class SonnenBatterieConsumptionAccessory<P extends PlatformAccessory>

// eslint-disable-next-line @typescript-eslint/no-unused-vars
updateAccessory(batteryStatus: BatteryStatus, _: InverterStatus) {
this.platform.log.debug("updating Consumption accessory: ", batteryStatus.Consumption_W);
const hasConsumption = batteryStatus.Consumption_W > 0;
const level = batteryStatus.USOC;
const lowBattery = level < batteryStatus.BackupBuffer;

this.platform.sonnenMQTT.update(batteryStatus.Consumption_W, "Consumption");
this.platform.sonnenMQTT.update(batteryStatus.USOC, "USOC");

this.platform.sonnenMqtt.publish("Consumption", batteryStatus.Consumption_W, this.platform.log);
this.platform.sonnenMqtt.publish("USOC", batteryStatus.USOC, this.platform.log);
//Grid (+I/-E) MQTT topic to modulate charge rate based on excess power
this.platform.sonnenMQTT.update(batteryStatus.GridFeedIn_W * -1, "Grid");
this.platform.sonnenMqtt.publish("Grid", batteryStatus.GridFeedIn_W * -1, this.platform.log);

this.service.updateCharacteristic(
this.platform.Characteristic.On,
Expand Down
Loading

0 comments on commit b228ee2

Please sign in to comment.