forked from Aietes/node-red-contrib-harmony
-
Notifications
You must be signed in to change notification settings - Fork 5
Open
Description
this module don't work and stop node-red with version superior to 4.x
according to claude.ai you need to patch the hub.js file to add delay and catching error connection to your harmonyhub
here is the correction, it work fine for me :
const EventEmitter = require('events');
const Harmony = require('harmony-websocket');
const isPortReachable = require('is-port-reachable');
const DEFAULT_HUB_PORT = 8088;
const RECONNECT_DELAY = 5000;
const STARTUP_DELAY = 3000;
class Hub extends EventEmitter {
constructor(ip) {
super();
this.ip = ip;
this.config = false;
this.harmony = false;
this.activityId = false;
this.activityStatus = false;
this.automationState = false;
this.connecting = false;
this.reconnectTimer = null;
this.startupComplete = false;
// Délai de démarrage pour éviter les problèmes au reboot
setTimeout(() => {
this.startupComplete = true;
}, STARTUP_DELAY);
}
_connect() {
// Empêcher les connexions multiples simultanées
if (this.connecting) {
return Promise.reject(new Error('Connection already in progress'));
}
this.connecting = true;
this.close();
try {
this.harmony = new Harmony();
this.harmony.on('open', () => {
this.connecting = false;
this.emit('open');
});
this.harmony.on('close', () => {
this.connecting = false;
this.emit('close');
this._scheduleReconnect();
});
this.harmony.on('stateDigest', digest => this._onStateDigest(digest));
this.harmony.on('automationState', state => this._onAutomationState(state));
this.harmony.on('error', (err) => {
this.connecting = false;
this.emit('error', err);
});
return isPortReachable(DEFAULT_HUB_PORT, {
host: this.ip,
timeout: 2000
})
.then((result) => {
if (!result) {
this.connecting = false;
throw new Error('Hub not reachable at ' + this.ip);
}
})
.then(() => this.harmony.connect(this.ip))
.then(() => this.harmony.getCurrentActivity())
.then(activityId => {
this.activityId = activityId;
this.activityStatus = 4;
})
.then(() => this.harmony.getConfig())
.then(config => {
this.config = config;
})
.then(() => this.harmony.getAutomationCommands())
.then(state => this.automationState = state.data)
.then(() => {
this.connecting = false;
return this.config;
})
.catch(err => {
this.connecting = false;
this.close();
throw err;
});
} catch (err) {
this.connecting = false;
return Promise.reject(err);
}
}
_scheduleReconnect() {
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
}
this.reconnectTimer = setTimeout(() => {
if (!this.isConnected()) {
this._connect().catch(() => {
// Réessayer plus tard
});
}
}, RECONNECT_DELAY);
}
_onStateDigest(digest) {
try {
if (digest && digest.data) {
this.activityId = digest.data.activityId;
this.activityStatus = digest.data.activityStatus;
this.emit('stateDigest', {
activityId: this.activityId,
activityStatus: this.activityStatus
});
}
} catch (err) {
this.emit('error', new Error('Error processing stateDigest: ' + err.message));
}
}
_onAutomationState(state) {
try {
if (state && state.data) {
this.emit('automationState', state.data);
}
} catch (err) {
this.emit('error', new Error('Error processing automationState: ' + err.message));
}
}
isConnected() {
try {
return this.harmony && this.harmony.isOpened && this.harmony.isOpened();
} catch (err) {
return false;
}
}
reloadConfig() {
this.config = false;
if (!this.isConnected()) {
return this._connect();
}
return this.harmony.getConfig()
.then(config => {
this.config = config;
return this.config;
})
.catch(err => {
this.config = false;
throw err;
});
}
getConfig() {
if (this.config) {
return Promise.resolve(this.config);
} else {
return this._connect();
}
}
getActivities() {
return this.getConfig()
.then(config => {
if (!config || !config.data || !config.data.activity) {
throw new Error('Invalid config data');
}
return config.data.activity
.filter(activity => {
let commands = activity.controlGroup
.map(group => group.function)
.reduce((prev, curr) => prev.concat(curr), [])
.map(cmd => {
return {
action: cmd.action,
label: cmd.label
};
});
activity.commands = commands;
return true;
}).map(activity => {
return {
id: activity.id,
label: activity.label,
commands: activity.commands,
type: 'activity'
};
});
});
}
getActivityCommands(activityId) {
return this.getConfig()
.then(config => {
if (!config || !config.data || !config.data.activity) {
throw new Error('Invalid config data');
}
let activity = config.data.activity
.filter(act => act.id === activityId)
.pop();
if (!activity) {
throw new Error('Activity not found: ' + activityId);
}
return activity.controlGroup
.map(group => group.function)
.reduce((prev, curr) => prev.concat(curr), [])
.map(cmd => {
return {
action: cmd.action,
label: cmd.label
};
});
});
}
getDevices() {
return this.getConfig()
.then(config => {
if (!config || !config.data || !config.data.device) {
throw new Error('Invalid config data');
}
return config.data.device
.filter(dev => {
let commands = dev.controlGroup
.map(group => group.function)
.reduce((prev, curr) => prev.concat(curr), [])
.map(cmd => {
return {
action: cmd.action,
label: cmd.label
};
});
dev.commands = commands;
return true;
}).map(dev => {
return {
id: dev.id,
label: dev.label,
commands: dev.commands,
type: 'device'
};
});
});
}
getDeviceCommands(deviceId) {
return this.getConfig()
.then(config => {
if (!config || !config.data || !config.data.device) {
throw new Error('Invalid config data');
}
let device = config.data.device
.filter(dev => dev.id === deviceId)
.pop();
if (!device) {
throw new Error('Device not found: ' + deviceId);
}
return device.controlGroup
.map(group => group.function)
.reduce((prev, curr) => prev.concat(curr), [])
.map(cmd => {
return {
action: JSON.parse(cmd.action),
label: cmd.label
};
});
});
}
startActivity(activityId) {
// Attendre que le démarrage soit complet
if (!this.startupComplete) {
return Promise.reject(new Error('Hub still initializing, please wait...'));
}
let promise = () => {
if (!this.isConnected()) {
return Promise.reject(new Error('Hub not connected'));
}
return this.harmony.startActivity(activityId);
};
if (!this.isConnected()) {
return this._connect()
.then(() => promise())
.catch(err => {
throw new Error('Failed to start activity: ' + err.message);
});
}
return promise().catch(err => {
throw new Error('Failed to start activity: ' + err.message);
});
}
getCurrentActivity() {
let promise = () => {
if (!this.isConnected()) {
return Promise.reject(new Error('Hub not connected'));
}
return this.harmony.getCurrentActivity()
.then(activityId => {
this.activityId = activityId;
this.activityStatus = 4;
this.emit('stateDigest', {
activityId: this.activityId,
activityStatus: this.activityStatus
});
return this.activityId;
});
};
if (!this.isConnected()) {
return this._connect()
.then(() => promise())
.catch(err => {
throw new Error('Failed to get current activity: ' + err.message);
});
}
return promise().catch(err => {
throw new Error('Failed to get current activity: ' + err.message);
});
}
getAction(id, command) {
if (!id || !command) {
return false;
}
try {
return JSON.stringify({
command: command.replace(' ', ''),
type: 'IRCommand',
deviceId: id,
});
} catch (err) {
return false;
}
}
sendCommand(action, hold, repeat, delay) {
// PROTECTION CRITIQUE : Vérifier que le hub est prêt
if (!this.startupComplete) {
return Promise.reject(new Error('Hub still initializing, please wait...'));
}
if (!this.isConnected()) {
return this._connect()
.then(() => this._sendCommandInternal(action, hold, repeat, delay))
.catch(err => {
throw new Error('Failed to send command (not connected): ' + err.message);
});
}
return this._sendCommandInternal(action, hold, repeat, delay);
}
_sendCommandInternal(action, hold, repeat, delay) {
// PROTECTION CRITIQUE : Double vérification avant d'envoyer
if (!this.harmony || !this.isConnected()) {
return Promise.reject(new Error('Hub not connected or harmony object is null'));
}
let promise = Promise.resolve();
try {
for (let i = 0; i < repeat; i++) {
if (hold > 0) {
promise = promise
.then(() => {
if (!this.isConnected()) {
throw new Error('Connection lost during command execution');
}
return this.harmony.sendCommandWithDelay(action, hold);
})
.then(response => new Promise(resolve => setTimeout(() => resolve(response), delay)))
.catch(err => {
throw new Error('Command failed: ' + err.message);
});
} else {
promise = promise
.then(() => {
if (!this.isConnected()) {
throw new Error('Connection lost during command execution');
}
return this.harmony.sendCommand(action);
})
.then(response => new Promise(resolve => setTimeout(() => resolve(response), delay)))
.catch(err => {
throw new Error('Command failed: ' + err.message);
});
}
}
return promise;
} catch (err) {
return Promise.reject(new Error('Error building command chain: ' + err.message));
}
}
getAutomationCommands() {
if (this.automationState) {
return Promise.resolve(this.automationState);
} else {
return this._connect()
.then(() => this.automationState)
.catch(err => {
throw new Error('Failed to get automation commands: ' + err.message);
});
}
}
reloadAutomationCommands() {
this.automationState = false;
if (!this.isConnected()) {
return this._connect();
}
return this.harmony.getAutomationCommands()
.then(state => {
this.automationState = state;
return this.automationState;
})
.catch(err => {
this.automationState = false;
throw new Error('Failed to reload automation commands: ' + err.message);
});
}
sendAutomationCommand(action) {
if (!this.startupComplete) {
return Promise.reject(new Error('Hub still initializing, please wait...'));
}
let promise = () => {
if (!this.isConnected()) {
return Promise.reject(new Error('Hub not connected'));
}
return this.harmony.sendAutomationCommand(action);
};
if (!this.isConnected()) {
return this._connect()
.then(() => promise())
.catch(err => {
throw new Error('Failed to send automation command: ' + err.message);
});
}
return promise().catch(err => {
throw new Error('Failed to send automation command: ' + err.message);
});
}
close() {
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
this.reconnectTimer = null;
}
if (this.harmony) {
try {
this.harmony.removeAllListeners();
this.harmony.close();
} catch (err) {
// Ignorer les erreurs de fermeture
}
delete this.harmony;
this.harmony = false;
return true;
}
return false;
}
}
module.exports = Hub;
Metadata
Metadata
Assignees
Labels
No labels