UNPKG

iobroker.melcloud

Version:
862 lines (780 loc) 26.3 kB
"use strict"; /* * Created with @iobroker/create-adapter v1.24.1 */ // The adapter-core module gives you access to the core ioBroker functions const utils = require("@iobroker/adapter-core"); // Needed modules const cloudPlatform = require("./lib/melcloudPlatform"); const commonDefines = require("./lib/commonDefines"); let CloudPlatform = null; const stateValueCache = {}; // used to store all adapter state values to check for unchanged values class Melcloud extends utils.Adapter { /** * @param {Partial<ioBroker.AdapterOptions>} [options] */ constructor(options) { // @ts-expect-error no issue super({ ...options, name: "melcloud", }); this.on("ready", this.onReady.bind(this)); this.on("stateChange", this.onStateChange.bind(this)); this.on("unload", this.onUnload.bind(this)); this.deviceObjects = []; // array of all device objects this.currentKnownDeviceIDs = []; // array of all current known device IDs } async checkSettings() { this.log.debug("Checking adapter settings..."); if (this.config.melCloudEmail == null || this.config.melCloudEmail == "") { throw new Error("MELCloud username empty! Check settings."); } if (this.config.melCloudPassword == null || this.config.melCloudPassword == "") { throw new Error("MELCloud password empty! Check settings."); } // Minimum pollingInterval = 5 to prevent rate limiting if (this.config.pollingInterval < 5) { this.config.pollingInterval = 5; this.log.warn( "Polling interval can't be set lower than 5 minutes to avoid being throttled by the MELCloud servers. Now set to 5 minutes.", ); } if (this.config.ignoreSslErrors) { this.log.info("SSL errors are ignored when communicating with the cloud. This is potentially insecure!"); } } async setAdapterConnectionState(isConnected) { await this.setStateChangedAsync( `${commonDefines.AdapterDatapointIDs.Info}.${commonDefines.AdapterStateIDs.Connection}`, isConnected, true, ); await this.setForeignState(`system.adapter.${this.namespace}.connected`, isConnected, true); } async saveKnownDeviceIDs() { this.log.debug("Getting current known devices..."); const prefix = `${this.namespace}.${commonDefines.AdapterDatapointIDs.Devices}.`; const objects = await this.getAdapterObjectsAsync(); for (const id of Object.keys(objects)) { if (!id.startsWith(prefix)) { continue; } const deviceIdTemp = id.replace(prefix, ""); const deviceId = parseInt(deviceIdTemp.substring(0, deviceIdTemp.lastIndexOf(".")), 10); // Add each device only one time if (!isNaN(deviceId) && !this.currentKnownDeviceIDs.includes(deviceId)) { this.currentKnownDeviceIDs.push(deviceId); this.log.debug(`Found known device: ${deviceId}`); } } if (this.currentKnownDeviceIDs.length == 0) { this.log.debug("No known devices found."); } } async deleteMelDevice(id) { const prefix = `${this.namespace}.${commonDefines.AdapterDatapointIDs.Devices}.${id}`; const objects = await this.getAdapterObjectsAsync(); for (const id of Object.keys(objects)) { if (id.startsWith(prefix)) { const objID = id.replace(`${this.namespace}.`, ""); this.log.debug(`Deleting state '${objID}'`); await this.delObjectAsync(objID); } } } async initObjects() { this.log.debug("Initializing objects..."); await this.setObjectNotExistsAsync(commonDefines.AdapterDatapointIDs.Devices, { type: "folder", common: { name: "Devices", }, native: {}, }); //#region INFO await this.setObjectNotExistsAsync(commonDefines.AdapterDatapointIDs.Info, { type: "channel", common: { name: "Adapter information", }, native: {}, }); await this.setObjectNotExistsAsync( `${commonDefines.AdapterDatapointIDs.Info}.${commonDefines.AdapterStateIDs.Connection}`, { type: "state", common: { name: "Connection to cloud", type: "boolean", role: "indicator.connected", read: true, write: false, def: false, desc: "Indicates if connection to MELCloud was successful or not", }, native: {}, }, ); this.setAdapterConnectionState(false); //#endregion //#region REPORTS (cumulated for all supported devices) let reportsPrefix = `${commonDefines.AdapterDatapointIDs.Reports}`; await this.setObjectNotExistsAsync(reportsPrefix, { type: "channel", common: { name: "Cumulated report for all supported devices", }, native: {}, }); reportsPrefix += "."; await this.setObjectNotExistsAsync( reportsPrefix + commonDefines.CommonDeviceStateIDs.PowerConsumptionReportStartDate, { type: "state", common: { name: "Report start date (format: YYYY-MM-DD)", type: "string", role: "date", read: true, write: true, desc: "Report data will be collected starting at this date", }, native: {}, }, ); await this.setObjectNotExistsAsync( reportsPrefix + commonDefines.CommonDeviceStateIDs.PowerConsumptionReportEndDate, { type: "state", common: { name: "Report end date (format: YYYY-MM-DD)", type: "string", role: "date", read: true, write: true, desc: "Report data will be collected until this date", }, native: {}, }, ); await this.setObjectNotExistsAsync( reportsPrefix + commonDefines.CommonDeviceStateIDs.GetCumulatedPowerConsumptionReport, { type: "state", common: { name: "Get current power consumption report for all supported devices", type: "boolean", role: "button", read: false, write: true, def: false, desc: "Get current power consumption report for all supported devices", }, native: {}, }, ); let lastReportDataPrefix = `${commonDefines.AdapterDatapointIDs.Reports}.${commonDefines.AdapterDatapointIDs.LastReportData}`; await this.setObjectNotExistsAsync(lastReportDataPrefix, { type: "channel", common: { name: "Last report data for all supported devices", }, native: {}, }); lastReportDataPrefix += "."; const reportModes = [ commonDefines.AtaDeviceOperationModes.HEAT.id, commonDefines.AtaDeviceOperationModes.COOL.id, commonDefines.AtaDeviceOperationModes.AUTO.id, commonDefines.AtaDeviceOperationModes.VENT.id, commonDefines.AtaDeviceOperationModes.DRY.id, "HotWater", ]; reportModes.forEach(mode => { this.setObjectNotExistsAsync( lastReportDataPrefix + commonDefines.CommonDeviceStateIDs.TotalPowerConsumptionPrefix + mode, { type: "state", common: { name: `Total power consumption for mode '${mode}'`, type: "number", role: "value.power.consumption", min: 0, read: true, write: false, unit: "kWh", def: 0, desc: `Total power consumption for mode '${mode}'`, }, native: {}, }, ); if ( mode == commonDefines.AtaDeviceOperationModes.AUTO.id || mode == commonDefines.AtaDeviceOperationModes.DRY.id || mode == commonDefines.AtaDeviceOperationModes.VENT.id ) { return; } this.setObjectNotExistsAsync( lastReportDataPrefix + commonDefines.CommonDeviceStateIDs.TotalPowerProductionPrefix + mode, { type: "state", common: { name: `Total power production for mode '${mode}'`, type: "number", role: "value.power.consumption", min: 0, read: true, write: false, unit: "kWh", def: 0, desc: `Total power production for mode '${mode}'`, }, native: {}, }, ); }); await this.setObjectNotExistsAsync( lastReportDataPrefix + commonDefines.CommonDeviceStateIDs.TotalPowerConsumptionPrefix, { type: "state", common: { name: "Total power consumption for all modes", type: "number", role: "value.power.consumption", min: 0, read: true, write: false, unit: "kWh", def: 0, desc: "Total power consumption for all modes", }, native: {}, }, ); await this.setObjectNotExistsAsync( lastReportDataPrefix + commonDefines.CommonDeviceStateIDs.TotalPowerProductionPrefix, { type: "state", common: { name: "Total power production for all modes", type: "number", role: "value.power.consumption", min: 0, read: true, write: false, unit: "kWh", def: 0, desc: "Total power production for all modes", }, native: {}, }, ); await this.setObjectNotExistsAsync( lastReportDataPrefix + commonDefines.CommonDeviceStateIDs.TotalReportedMinutes, { type: "state", common: { name: "Total power consumption minutes", type: "number", role: "value", min: 0, read: true, write: false, unit: "min", def: 0, desc: "Total operation time", }, native: {}, }, ); //#endregion } /** * Is called when databases are connected and adapter received configuration. */ async onReady() { this.initObjects() .then(() => this.checkSettings().then(() => this.saveKnownDeviceIDs().then(() => { this.connectToCloud(); this.subscribeStates("devices.*.control.*"); // subscribe to states changes under "devices.X.control." this.subscribeStates("devices.*.reports.getPowerConsumptionReport"); // subscribe to state "devices.X.reports.getPowerConsumptionReport" this.subscribeStates("reports.getCumulatedPowerConsumptionReport"); // subscribe to state "reports.getCumulatedPowerConsumptionReport" }), ), ) .catch(err => this.log.error(err)); } async connectToCloud() { this.log.info( `Connecting initially to MELCloud and retrieving device data. Polling is ${this.config.enablePolling ? `enabled (interval: ${this.config.pollingInterval} minutes)` : "disabled"}.`, ); // Connect to cloud and retrieve/update registered devices initially CloudPlatform = new cloudPlatform.MelCloudPlatform(this); if (this.config.enablePolling) { CloudPlatform.GetContextKey( CloudPlatform.CreateAndSaveDevices.bind(CloudPlatform), CloudPlatform.startPolling.bind(CloudPlatform), ); } else { CloudPlatform.GetContextKey(CloudPlatform.CreateAndSaveDevices.bind(CloudPlatform)); } } /** * Is called when adapter shuts down - callback has to be called under any circumstances! * @param {() => void} callback */ onUnload(callback) { try { this.setAdapterConnectionState(false); this.deviceObjects.length = 0; if (CloudPlatform != null) { CloudPlatform.stopPolling(); CloudPlatform.stopContextKeyInvalidation(); } this.log.debug("onUnload(): Cleaned everything up..."); callback(); } catch { callback(); } } /** * Is called if a subscribed state changes * @param {string} id * @param {ioBroker.State | null | undefined} state */ onStateChange(id, state) { if (state) { // The state was changed // always trigger on "getPowerConsumptionReport" and "getCumulatedPowerConsumptionReport" ignoring if the value has changed or not if ( !id.includes(commonDefines.CommonDeviceStateIDs.GetPowerConsumptionReport) && !id.includes(commonDefines.CommonDeviceStateIDs.GetCumulatedPowerConsumptionReport) && stateValueCache[id] != undefined && stateValueCache[id] != null && stateValueCache[id] == state.val ) { this.log.silly(`state ${id} unchanged: ${state.val} (ack = ${state.ack})`); return; } stateValueCache[id] = state.val; this.log.silly(`state ${id} changed: ${state.val} (ack = ${state.ack})`); // ack is true when state was updated by MELCloud --> in this case, we don't need to send it again if (state.ack) { this.log.silly("Updated data was retrieved from MELCloud. No need to process changed data."); return; } if (this.deviceObjects.length == 0) { this.log.error("No objects for MELCloud devices constructed yet. Try again in a few seconds..."); return; } if (id.includes(commonDefines.CommonDeviceStateIDs.GetCumulatedPowerConsumptionReport)) { // "reports.getCumulatedPowerConsumptionReport" this.log.debug( `Processing command '${commonDefines.CommonDeviceStateIDs.GetCumulatedPowerConsumptionReport}' with value '${state.val}' for all devices...`, ); this.GetCumulatedReport(); } else { // "devices.XXX.control.*" and "devices.XXX.reports.getPowerConsumptionReport" let deviceId = id.replace(`${this.namespace}.${commonDefines.AdapterDatapointIDs.Devices}.`, ""); deviceId = deviceId.substring(0, deviceId.indexOf(".")); // Get the device object that should be changed this.log.debug(`Trying to get device object with id ${deviceId}...`); const device = this.deviceObjects.find(obj => { return obj.id === parseInt(deviceId); }); if (device == null) { let knownIds = ""; this.deviceObjects.forEach(obj => (knownIds += `${obj.id}, `)); this.log.error(`Failed to get device object. Known object IDs: ${knownIds}`); this.log.error("This should not happen - report this to the developer!"); return; } const controlOption = id.substring(id.lastIndexOf(".") + 1, id.length); this.log.debug( `Processing command '${controlOption}' with value '${state.val}' for device object with id ${device.id} (${device.name})...`, ); const type = device.deviceType; const handlers = { [commonDefines.DeviceTypes.AirToAir]: this.processAtaDeviceCommand, [commonDefines.DeviceTypes.AirToWater]: this.processAtwDeviceCommand, [commonDefines.DeviceTypes.EnergyRecoveryVentilation]: this.processErvDeviceCommand, }; const handler = handlers[type]; if (handler) { handler.call(this, controlOption, state, device); } else { this.log.error(`Unsupported device type: '${type}' - Please report this to the developer!`); } } } else { // The state was deleted this.log.silly(`state ${id} deleted`); if (stateValueCache[id]) { delete stateValueCache[id]; } } } async GetCumulatedReport() { const promises = []; for (const obj of this.deviceObjects) { promises.push(obj.getPowerConsumptionReport(true)); } Promise.all(promises).then(() => { this.UpdateCumulatedReportData(this.deviceObjects); }); } async UpdateCumulatedReportData(deviceObjs) { const cumulatedLastReportDataPrefix = `${commonDefines.AdapterDatapointIDs.Reports}.${commonDefines.AdapterDatapointIDs.LastReportData}.`; let totalConsumption = 0, totalConsumptionCool = 0, totalConsumptionHeat = 0, totalConsumptionDry = 0, totalConsumptionVent = 0, totalConsumptionAuto = 0, totalConsumptionMinutes = 0; const aggregatedDeviceGroups = []; for (const obj of deviceObjs) { // Check if device is already part of aggregation group to exclude duplicated values if ( obj.linkedDevicesIncludedInArregateEnergyReport && obj.linkedDevicesIncludedInArregateEnergyReport != "" ) { if (aggregatedDeviceGroups.length == 0) { aggregatedDeviceGroups.push({ groupName: obj.linkedDevicesIncludedInArregateEnergyReport, alreadyProcessed: false, }); } let isKnownGroup = false; for (let i = 0; i < aggregatedDeviceGroups.length; i++) { const aggregatedGroup = aggregatedDeviceGroups[i]; if (aggregatedGroup.groupName.includes(obj.name) && !aggregatedGroup.alreadyProcessed) { this.log.debug( `Device '${obj.name}' is part of the aggregated group '${aggregatedGroup.groupName}'. Excluding the other devices from this group for cumulated reports.`, ); totalConsumptionCool += obj.totalPowerConsumptionCooling; totalConsumptionHeat += obj.totalPowerConsumptionHeating; totalConsumptionAuto += obj.totalPowerConsumptionAuto; totalConsumptionDry += obj.totalPowerConsumptionDry; totalConsumptionVent += obj.totalPowerConsumptionVent; totalConsumptionMinutes = obj.totalPowerConsumptionMinutes; // same for all devices totalConsumption += totalConsumptionCool + totalConsumptionHeat + totalConsumptionAuto + totalConsumptionDry + totalConsumptionVent; aggregatedGroup.alreadyProcessed = true; isKnownGroup = true; break; } } if (!isKnownGroup) { aggregatedDeviceGroups.push({ groupName: obj.linkedDevicesIncludedInArregateEnergyReport, alreadyProcessed: false, }); } } else { // Device is not part of any aggregation group, just take the values as they are totalConsumptionCool += obj.totalPowerConsumptionCooling; totalConsumptionHeat += obj.totalPowerConsumptionHeating; totalConsumptionAuto += obj.totalPowerConsumptionAuto; totalConsumptionDry += obj.totalPowerConsumptionDry; totalConsumptionVent += obj.totalPowerConsumptionVent; totalConsumptionMinutes += obj.totalPowerConsumptionMinutes; totalConsumption += totalConsumptionCool + totalConsumptionHeat + totalConsumptionAuto + totalConsumptionDry + totalConsumptionVent; } } await this.setStateChangedAsync( cumulatedLastReportDataPrefix + commonDefines.CommonDeviceStateIDs.TotalPowerConsumptionPrefix + commonDefines.AtaDeviceOperationModes.COOL.id, commonDefines.roundValue(totalConsumptionCool, 3), true, ); await this.setStateChangedAsync( cumulatedLastReportDataPrefix + commonDefines.CommonDeviceStateIDs.TotalPowerConsumptionPrefix + commonDefines.AtaDeviceOperationModes.HEAT.id, commonDefines.roundValue(totalConsumptionHeat, 3), true, ); await this.setStateChangedAsync( cumulatedLastReportDataPrefix + commonDefines.CommonDeviceStateIDs.TotalPowerConsumptionPrefix + commonDefines.AtaDeviceOperationModes.AUTO.id, commonDefines.roundValue(totalConsumptionAuto, 3), true, ); await this.setStateChangedAsync( cumulatedLastReportDataPrefix + commonDefines.CommonDeviceStateIDs.TotalPowerConsumptionPrefix + commonDefines.AtaDeviceOperationModes.DRY.id, commonDefines.roundValue(totalConsumptionDry, 3), true, ); await this.setStateChangedAsync( cumulatedLastReportDataPrefix + commonDefines.CommonDeviceStateIDs.TotalPowerConsumptionPrefix + commonDefines.AtaDeviceOperationModes.VENT.id, commonDefines.roundValue(totalConsumptionVent, 3), true, ); await this.setStateChangedAsync( cumulatedLastReportDataPrefix + commonDefines.CommonDeviceStateIDs.TotalPowerConsumptionPrefix, commonDefines.roundValue(totalConsumption, 3), true, ); await this.setStateChangedAsync( cumulatedLastReportDataPrefix + commonDefines.CommonDeviceStateIDs.TotalReportedMinutes, totalConsumptionMinutes, true, ); this.log.debug(`Updated cumulated report data for all devices`); } mapAtaDeviceOperationMode(value) { return this.mapDeviceOperationMode(value, commonDefines.AtaDeviceOperationModes, "ATA"); } mapAtwDeviceOperationMode(value) { return this.mapDeviceOperationMode(value, commonDefines.AtwDeviceOperationModes, "ATW"); } mapERVDeviceOperationMode(value) { return this.mapDeviceOperationMode(value, commonDefines.ErvDeviceOperationModes, "ERV"); } mapDeviceOperationMode(value, enumObject, deviceName) { const numValue = Number(value); if (isNaN(numValue)) { this.log.error(`Invalid ${deviceName} operation mode type: '${value}' (not a number)`); return enumObject.UNDEF; } const foundMode = Object.values(enumObject).find(mode => mode.value === numValue); if (!foundMode) { this.log.error(`Unsupported ${deviceName} operation mode: '${value}'`); return enumObject.UNDEF; } return foundMode; } processAtaDeviceCommand(controlOption, state, device) { switch (controlOption) { case commonDefines.AtaDeviceStateIDs.Power: if (state.val) { // switch on using current operation mode device.getDeviceInfo( device.setDevice.bind(device), commonDefines.AtaDeviceOptions.PowerState, commonDefines.DevicePowerStates.ON, ); } else { // switch off device.getDeviceInfo( device.setDevice.bind(device), commonDefines.AtaDeviceOptions.PowerState, commonDefines.DevicePowerStates.OFF, ); } break; case commonDefines.AtaDeviceStateIDs.Mode: device.getDeviceInfo( device.setDevice.bind(device), commonDefines.AtaDeviceOptions.TargetHeatingCoolingState, this.mapAtaDeviceOperationMode(state.val), ); break; case commonDefines.AtaDeviceStateIDs.TargetTemp: device.getDeviceInfo( device.setDevice.bind(device), commonDefines.AtaDeviceOptions.TargetTemperature, state.val, ); break; case commonDefines.AtaDeviceStateIDs.FanSpeedManual: device.getDeviceInfo(device.setDevice.bind(device), commonDefines.AtaDeviceOptions.FanSpeed, state.val); break; case commonDefines.AtaDeviceStateIDs.VaneVerticalDirection: device.getDeviceInfo( device.setDevice.bind(device), commonDefines.AtaDeviceOptions.VaneVerticalDirection, state.val, ); break; case commonDefines.AtaDeviceStateIDs.VaneHorizontalDirection: device.getDeviceInfo( device.setDevice.bind(device), commonDefines.AtaDeviceOptions.VaneHorizontalDirection, state.val, ); break; case commonDefines.CommonDeviceStateIDs.TimerToogle: device.toggleTimerState(state.val); break; case commonDefines.CommonDeviceStateIDs.GetPowerConsumptionReport: device.getPowerConsumptionReport(); break; default: this.log.error( `Unsupported ATA control option: ${controlOption} - Please report this to the developer!`, ); break; } } processAtwDeviceCommand(controlOption, state, device) { switch (controlOption) { case commonDefines.AtwDeviceStateIDs.Power: if (state.val) { // switch on using current operation mode device.getDeviceInfo( device.setDevice.bind(device), commonDefines.AtwDeviceOptions.PowerState, commonDefines.DevicePowerStates.ON, ); } else { // switch off device.getDeviceInfo( device.setDevice.bind(device), commonDefines.AtwDeviceOptions.PowerState, commonDefines.DevicePowerStates.OFF, ); } break; case commonDefines.AtwDeviceStateIDs.ForcedHotWaterMode: device.getDeviceInfo( device.setDevice.bind(device), commonDefines.AtwDeviceOptions.ForcedHotWaterMode, state.val, ); break; case commonDefines.AtwDeviceStateIDs.OperationModeZone1: device.getDeviceInfo( device.setDevice.bind(device), commonDefines.AtwDeviceOptions.OperationModeZone1, this.mapAtwDeviceZoneOperationMode(state.val), ); break; case commonDefines.AtwDeviceStateIDs.OperationModeZone2: device.getDeviceInfo( device.setDevice.bind(device), commonDefines.AtwDeviceOptions.OperationModeZone2, this.mapAtwDeviceZoneOperationMode(state.val), ); break; case commonDefines.AtwDeviceStateIDs.SetTankWaterTemperature: device.getDeviceInfo( device.setDevice.bind(device), commonDefines.AtwDeviceOptions.SetTankWaterTemperature, state.val, ); break; case commonDefines.AtwDeviceStateIDs.SetTemperatureZone1: device.getDeviceInfo( device.setDevice.bind(device), commonDefines.AtwDeviceOptions.SetTemperatureZone1, state.val, ); break; case commonDefines.AtwDeviceStateIDs.SetTemperatureZone2: device.getDeviceInfo( device.setDevice.bind(device), commonDefines.AtwDeviceOptions.SetTemperatureZone2, state.val, ); break; case commonDefines.AtwDeviceStateIDs.SetHeatFlowTemperatureZone1: device.getDeviceInfo( device.setDevice.bind(device), commonDefines.AtwDeviceOptions.SetHeatFlowTemperatureZone1, state.val, ); break; case commonDefines.AtwDeviceStateIDs.SetHeatFlowTemperatureZone2: device.getDeviceInfo( device.setDevice.bind(device), commonDefines.AtwDeviceOptions.SetHeatFlowTemperatureZone2, state.val, ); break; case commonDefines.AtwDeviceStateIDs.SetCoolFlowTemperatureZone1: device.getDeviceInfo( device.setDevice.bind(device), commonDefines.AtwDeviceOptions.SetCoolFlowTemperatureZone1, state.val, ); break; case commonDefines.AtwDeviceStateIDs.SetCoolFlowTemperatureZone2: device.getDeviceInfo( device.setDevice.bind(device), commonDefines.AtwDeviceOptions.SetCoolFlowTemperatureZone2, state.val, ); break; case commonDefines.CommonDeviceStateIDs.TimerToogle: device.toggleTimerState(state.val); break; case commonDefines.CommonDeviceStateIDs.GetPowerConsumptionReport: device.getPowerConsumptionReport(); break; default: this.log.error( `Unsupported ATW control option: ${controlOption} - Please report this to the developer!`, ); break; } } processErvDeviceCommand(controlOption, state, device) { switch (controlOption) { case commonDefines.ErvDeviceStateIDs.Power: if (state.val) { // switch on using current operation mode device.getDeviceInfo( device.setDevice.bind(device), commonDefines.ErvDeviceOptions.PowerState, commonDefines.DevicePowerStates.ON, ); } else { // switch off device.getDeviceInfo( device.setDevice.bind(device), commonDefines.ErvDeviceOptions.PowerState, commonDefines.DevicePowerStates.OFF, ); } break; case commonDefines.ErvDeviceStateIDs.Mode: device.getDeviceInfo( device.setDevice.bind(device), commonDefines.ErvDeviceOptions.OperationMode, this.mapERVDeviceOperationMode(state.val), ); break; case commonDefines.ErvDeviceStateIDs.FanSpeed: device.getDeviceInfo(device.setDevice.bind(device), commonDefines.ErvDeviceOptions.FanSpeed, state.val); break; default: this.log.error( `Unsupported ERV control option: ${controlOption} - Please report this to the developer!`, ); break; } } } // @ts-expect-error parent is a valid property on module if (module.parent) { // Export the constructor in compact mode /** * @param {Partial<ioBroker.AdapterOptions>} [options] */ module.exports = options => new Melcloud(options); } else { // otherwise start the instance directly new Melcloud(); }