Skip to content

Commit

Permalink
Merge Object state cleanup to main branch, resolved #39
Browse files Browse the repository at this point in the history
  • Loading branch information
DutchmanNL authored Nov 3, 2023
2 parents 881d8e5 + a181dcd commit a567cc1
Show file tree
Hide file tree
Showing 3 changed files with 212 additions and 7 deletions.
77 changes: 77 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
name: Test and Release

# Run this job on all pushes and pull requests
# as well as tags with a semantic version
on:
push:
branches:
- "*"
tags:
# normal versions
- "v[0-9]+.[0-9]+.[0-9]+"
# pre-releases
- "v[0-9]+.[0-9]+.[0-9]+-**"
pull_request: {}

jobs:
# Performs quick checks before the expensive test runs
check-and-lint:
if: contains(github.event.head_commit.message, '[skip ci]') == false

runs-on: ubuntu-latest

strategy:
matrix:
node-version: [18.x]

steps:
- name: Checkout code
uses: actions/checkout@v2

- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}

- name: Install Dependencies
run: npm ci

- name: Lint source code
run: npm run lint
- name: Test package files
run: npm run test:package

# Runs adapter tests on all supported node versions and OSes
adapter-tests:
if: contains(github.event.head_commit.message, '[skip ci]') == false

needs: [check-and-lint]

runs-on: ${{ matrix.os }}
strategy:
matrix:
node-version: [16.x, 18.x, 20.x]
os: [ubuntu-latest, windows-latest, macos-latest]

steps:
- name: Checkout code
uses: actions/checkout@v2

- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}

- name: Install Dependencies
run: npm ci

- name: Run unit tests
run: npm run test:unit

# - name: Run integration tests (unix only)
# if: startsWith(runner.OS, 'windows') == false
# run: DEBUG=testing:* npm run test:integration
#
# - name: Run integration tests (windows only)
# if: startsWith(runner.OS, 'windows')
# run: set DEBUG=testing:* & npm run test:integration
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,21 +114,23 @@ If you like my work, please consider a personal donation
-->

### __WORK IN PROGRESS__
* (DutchmanNL) Added cleanup capability for unused channels & states after initialisation of device, resolves #39
* (DutchmanNL) Added button to info channel which allows to delete all offline devices from adapter tree. resolves #39
* (DutchmanNL) [Breaking] Backup strategy changed, requires [BackitUp v2.9.1](https://github.com/simatec/ioBroker.backitup) and activate option for ESPHome, fixes #129

### 0.3.2 (2023-11-01)
* (DutchmanNL) Improved error handling if devices are not reachable/disconnected
* (DutchmanNL) Bugfix: Allow control of brightness and color for light component, fixes #173
* (DutchmanNL) Bugfix: Allow control of brightness and color for light component, resolves #173

### 0.3.1 (2023-10-31)
* (DutchmanNL) Bugfix: Show online state of ESP Device correctly, Fixes #106
* (DutchmanNL) Bugfix: Show online state of ESP Device correctly, resolves #106

### 0.3.0 (2023-10-31) - Bugfixes & Improvements
* (Dutchman & SimonFischer04) Several Bugfixes
* (SimonFischer04) Support type "select device"
* (DutchmanNL) ESPHome dashboard default disabled
* (SimonFischer04) Migrate to @2colors/esphome-native-api
* (DutchmanNL) Automatically create needed directories, Fixes #168
* (DutchmanNL) Automatically create needed directories, resolves #168
* (SimonFischer04) Migrate usage of python to new structure, should solve all ESPHome Dashboard related installation issues

### 0.2.4 (2021-08-24)
Expand Down
134 changes: 130 additions & 4 deletions main.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@ const stateAttr = require(__dirname + '/lib/stateAttr.js'); // Load attribute li
const disableSentry = false; // Ensure to set to true during development!
const warnMessages = {}; // Store warn messages to avoid multiple sending to sentry
const fs = require('fs');
const {clearTimeout} = require('timers');
const client = {};
let reconnectTimer, reconnectInterval, apiPass, autodiscovery, dashboardProcess, createConfigStates;
const resetTimers = {}; // Memory allocation for all running timers
let reconnectInterval, apiPass, autodiscovery, dashboardProcess, createConfigStates;

class Esphome extends utils.Adapter {

Expand Down Expand Up @@ -236,6 +238,14 @@ class Esphome extends utils.Adapter {
if (this.deviceInfo[host].deviceName != null) {
this.setState(`${this.deviceInfo[host].deviceName}.info._online`, {val: false, ack: true});

// Check all created states in memory if their are related to this device
for (const state in this.createdStatesDetails) {
// Remove states from cache
if (state.split('.')[0] === this.deviceInfo[host].deviceName) {
delete this.createdStatesDetails[state];
}
}

// Cache relevant data before clearing memory space of device
const cacheDeviceInformation = {
deviceName: this.deviceInfo[host].deviceName,
Expand Down Expand Up @@ -265,6 +275,12 @@ class Esphome extends utils.Adapter {
client[host].on('initialized', () => {
this.log.info(`ESPHome client ${this.deviceInfo[host].deviceInfoName} on ip ${host} initialized`);
this.deviceInfo[host].initialized = true;

// Start timer to cleanup unneeded objects
if (resetTimers[host]) resetTimers[host] = clearTimeout(resetTimers[host]);
resetTimers[host] = setTimeout(async () => {
await this.objectCleanup(host);
}, (10000));
});

// Log message listener
Expand All @@ -285,7 +301,12 @@ class Esphome extends utils.Adapter {
// Store device information into memory
const deviceName = this.replaceAll(deviceInfo.macAddress, `:`, ``);
this.deviceInfo[host] = {
adapterObjects : {
channels : []
},
ip: host,
connectError : false,
connected : true,
mac: deviceInfo.macAddress,
deviceInfo: deviceInfo,
deviceName: deviceName,
Expand Down Expand Up @@ -367,6 +388,11 @@ class Esphome extends utils.Adapter {
native: {},
});

// Cache created channel in device memory
if (!this.deviceInfo[host].adapterObjects.channels.includes(`${this.namespace}.${this.deviceInfo[host].deviceName}.${entity.type}`)) {
this.deviceInfo[host].adapterObjects.channels.push(`${this.namespace}.${this.deviceInfo[host].deviceName}.${entity.type}`);
}

// Create state specific channel by id
await this.extendObjectAsync(`${this.deviceInfo[host].deviceName}.${entity.type}.${entity.id}`, {
type: 'channel',
Expand All @@ -376,6 +402,11 @@ class Esphome extends utils.Adapter {
native: {},
});

// Cache created channel in device memory
if (!this.deviceInfo[host].adapterObjects.channels.includes(`${this.namespace}.${this.deviceInfo[host].deviceName}.${entity.type}.${entity.id}`)) {
this.deviceInfo[host].adapterObjects.channels.push(`${this.namespace}.${this.deviceInfo[host].deviceName}.${entity.type}.${entity.id}`);
}

//Check if config channel should be created
if (!createConfigStates) {
// Delete folder structure if already present
Expand All @@ -396,6 +427,12 @@ class Esphome extends utils.Adapter {
},
native: {},
});

// Cache created channel in device memory
if (!this.deviceInfo[host].adapterObjects.channels.includes(`${this.namespace}.${this.deviceInfo[host].deviceName}.${entity.type}.${entity.id}.config`)) {
this.deviceInfo[host].adapterObjects.channels.push(`${this.namespace}.${this.deviceInfo[host].deviceName}.${entity.type}.${entity.id}.config`);
}

// Handle Entity JSON structure and write related config channel data
await this.TraverseJson(entity.config, `${this.deviceInfo[host].deviceName}.${entity.type}.${entity.id}.config`);
}
Expand Down Expand Up @@ -491,7 +528,7 @@ class Esphome extends utils.Adapter {


} catch (e) {
this.log.error(`Connection issue for ${entity.name} ${e}`);
this.log.error(`Connection issue for ${entity.name} ${e} | ${e.stack}`);
}

});
Expand Down Expand Up @@ -671,6 +708,8 @@ class Esphome extends utils.Adapter {
// Check if state contains value
if (transitionLength) {
this.deviceInfo[host][entity.id].states.transitionLength = transitionLength.val;
// Run create state routine to ensure state is cached in memory
await this.stateSetCreate(`${this.deviceInfo[host].deviceName}.${entity.type}.${entity.id}.transitionLength`, `${stateName} of ${entity.config.name}`, transitionLength.val, `s`, writable);
} else { // Else just create it
await this.stateSetCreate(`${this.deviceInfo[host].deviceName}.${entity.type}.${entity.id}.transitionLength`, `${stateName} of ${entity.config.name}`, 0, `s`, writable);
this.deviceInfo[host][entity.id].states.transitionLength = 0;
Expand Down Expand Up @@ -947,6 +986,8 @@ class Esphome extends utils.Adapter {
onUnload(callback) {
try {
this.log.debug(JSON.stringify(this.deviceInfo));

// Set all online states to false
for (const device in this.deviceInfo) {

// Ensure all known online states are set to false
Expand All @@ -961,9 +1002,12 @@ class Esphome extends utils.Adapter {
this.log.debug(`[onUnload] ${JSON.stringify(e)}`);
}
}
if (reconnectTimer) {
reconnectTimer = clearTimeout();

// Ensure all possible running timers are cleared
for (const timer in resetTimers) {
if (resetTimers[timer]) resetTimers[timer] = clearTimeout(resetTimers[timer]);
}

try {
if (dashboardProcess) {
dashboardProcess.kill('SIGTERM', {
Expand Down Expand Up @@ -1078,6 +1122,17 @@ class Esphome extends utils.Adapter {
try {
if (state && state.ack === false) {
const device = id.split('.');

try {
// Verify if trigger is related to device-cleanup
if (id.split('.')[3] === 'deviceCleanup'){
await this.offlineDeviceCleanup();
return;
}
} catch (e) {
// Skip action
}

const deviceIP = this.deviceStateRelation[device[2]].ip;

// Handle Switch State
Expand Down Expand Up @@ -1232,6 +1287,77 @@ class Esphome extends utils.Adapter {
this.log.error(`[resetOnlineState] ${e}`);
}
}

async objectCleanup(ip){
try {
this.log.debug(`[objectCleanup] Starting channel and state cleanup for ${this.deviceInfo[ip].deviceName} | ${ip} | ${this.deviceInfo[ip].ip}`);

// Cancel cleanup operation in case device is not connected anymore
if (this.deviceInfo[ip].connectionError || !this.deviceInfo[ip].connected) return;

// Set parameters for object view to only include objects within adapter namespace
const params = {
startkey : `${this.namespace}.${this.deviceInfo[ip].deviceName}.`,
endkey : `${this.namespace}.\u9999`,
};

// Get all current channels
const _channels = await this.getObjectViewAsync('system', 'channel', params);
// List all found channels & compare with memory, delete unneeded channels
for (const currDevice in _channels.rows) {
// @ts-ignore
if (!this.deviceInfo[ip].adapterObjects.channels.includes(_channels.rows[currDevice].id)
&& _channels.rows[currDevice].id.split('.')[2] === this.deviceInfo[ip].deviceName){
this.log.debug(`[objectCleanup] Unknown Channel found, delete ${_channels.rows[currDevice].id}`);
await this.delObjectAsync(_channels.rows[currDevice].id, {recursive: true});
}
}

// Get all current states in adapter tree
const _states = await this.getObjectViewAsync('system', 'state', params);
// List all found states & compare with memory, delete unneeded states
for (const currDevice in _states.rows) {
if (!this.createdStatesDetails[_states.rows[currDevice].id.replace(`esphome.0.`, ``)]
&& _states.rows[currDevice].id.split('.')[2] === this.deviceInfo[ip].deviceName){
this.log.debug(`[objectCleanup] Unknown State found, delete ${_states.rows[currDevice].id}`);
// await this.delObjectAsync(_states.rows[currDevice].id);
}
}

} catch (e) {
this.log.error(`[objectCleanup] Fatal error ${e} | ${e.stack}`);
}
}

async offlineDeviceCleanup () {
this.log.info(`Offline Device cleanup started`);

try {

// Get an overview of all current devices known by adapter
const knownDevices = await this.getDevicesAsync();
console.log(`KnownDevices: ${knownDevices}`);

// Loop to all devices, check if online state = TRUE otherwise delete device
for (const device in knownDevices){

// Get online value
const online = await this.getStateAsync(`${knownDevices[device]._id}.info._online`);
this.log.info(`Online state ${JSON.stringify(online)}`);
if (!online || !online.val){
this.log.info(`Offline device ${knownDevices[device]._id.split('.')[2]} expected on ip ${knownDevices[device].native.ip} removed`);
}

}

if (!knownDevices) return; // exit function if no known device are detected
if (knownDevices.length > 0) this.log.info(`Try to contact ${knownDevices.length} known devices`);
} catch (e) {
this.log.error(`[offlineDeviceCleanup] Fatal error occured, cannot cleanup offline devices ${e} | ${e.stack}`);

}
}

}

if (require.main !== module) {
Expand Down

0 comments on commit a567cc1

Please sign in to comment.