├── .gitignore ├── LICENSE ├── package.json ├── README.md ├── CHANGELOG.md ├── config.schema.json └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Marcin Laber 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "displayName": "Shelly Energy Meter Platform", 3 | "name": "homebridge-shelly-energy-meter", 4 | "version": "2.0.0", 5 | "description": "Homebridge platform plugin for multiple Shelly 3EM/EM energy meters in HomeKit via EVE app", 6 | "license": "MIT", 7 | "homepage": "https://github.com/thechris1992/homebridge-shelly-energy-meter#readme", 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/thechris1992/homebridge-shelly-energy-meter.git" 11 | }, 12 | "bugs": { 13 | "url": "https://github.com/thechris1992/homebridge-shelly-energy-meter/issues" 14 | }, 15 | "engines": { 16 | "node": ">=18.0.0", 17 | "homebridge": ">=1.6.0" 18 | }, 19 | "main": "index.js", 20 | "keywords": [ 21 | "homebridge-plugin", 22 | "homebridge", 23 | "shelly", 24 | "energy-meter", 25 | "platform", 26 | "eve" 27 | ], 28 | "author": "thechris1992", 29 | "dependencies": { 30 | "fakegato-history": "^0.6.3", 31 | "node-fetch": "^2.7.0" 32 | }, 33 | "peerDependencies": { 34 | "homebridge": ">=1.6.0" 35 | }, 36 | "homebridge": { 37 | "displayName": "Shelly Energy Meter Platform", 38 | "platformName": "ShellyEnergyMeter", 39 | "configSchema": "config.schema.json" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Homebridge 3EM Energy Meter Platform 2 | 3 | [![verified-by-homebridge](https://badgen.net/badge/homebridge/verified/purple)](https://github.com/homebridge/homebridge/wiki/Verified-Plugins) 4 | [![npm version](https://badge.fury.io/js/homebridge-3em-energy-meter.svg)](https://www.npmjs.com/package/homebridge-3em-energy-meter) 5 | 6 | A Homebridge platform plugin for monitoring multiple Shelly 3EM and EM energy meters in HomeKit via the EVE app. 7 | 8 | ## Features 9 | 10 | - **Fixed Phase Support**: 3EM devices always use 3 phases, EM devices always use 2 phases 11 | - **Multiple Device Types**: Supports both Shelly 3EM and Shelly EM devices 12 | - **Multiple Device Support**: Monitor multiple devices simultaneously 13 | - **Robust Handling**: Missing phases are automatically set to 0 (no errors) 14 | - **EVE App Integration**: View energy data in the EVE app (required for energy characteristics) 15 | - **History Support**: Historical data with fakegato-history 16 | - **Authentication**: Support for password-protected devices 17 | 18 | ## Installation 19 | 20 | ### Standard Installation 21 | ```bash 22 | npm install thechris1992/homebridge-shelly-energy-meter 23 | ``` 24 | 25 | ### Install a branch #... with Debug Output 26 | ```bash 27 | npm install thechris1992/homebridge-shelly-energy-meter#feature/plattform --install-strategy shallow --loglevel verbose 28 | ``` 29 | 30 | **Note**: After installation, restart Homebridge to load the plugin. 31 | 32 | ## Configuration 33 | 34 | Add to your Homebridge config.json platforms section: 35 | 36 | ```json 37 | { 38 | "platform": "ShellyEnergyMeter", 39 | "name": "Energy Meters", 40 | "devices": [ 41 | { 42 | "name": "Main House Meter", 43 | "ip": "192.168.1.100" 44 | }, 45 | { 46 | "name": "Garage Meter", 47 | "ip": "192.168.1.101", 48 | "auth": { 49 | "user": "admin", 50 | "pass": "password" 51 | } 52 | }, 53 | { 54 | "name": "Solar Meter", 55 | "ip": "192.168.1.102", 56 | "device_type": "EM", 57 | "use_em_mode": 0, 58 | "debug_log": true 59 | } 60 | ] 61 | } 62 | ``` 63 | 64 | ### Configuration Options 65 | 66 | #### Platform Level 67 | - `platform`: Must be `"ShellyEnergyMeter"` 68 | - `name`: Platform name (shown in logs) 69 | - `devices`: Array of device configurations 70 | 71 | #### Device Level 72 | - `name` (required): Device name in HomeKit 73 | - `ip` (required): Device IP address 74 | - `device_type` (optional): Device type - "3EM" (3 phases) or "EM" (2 phases) (default: "3EM") 75 | - `auth` (optional): Authentication credentials 76 | - `user`: Username 77 | - `pass`: Password 78 | - `timeout` (optional): HTTP timeout in ms (default: 5000) 79 | - `update_interval` (optional): Update interval in ms (default: 10000) 80 | - `use_em_mode` (optional): EM channel mode (0=all, 1=ch1, 2=ch2) - only for EM devices 81 | - `use_pf` (optional): Use power factor for current calculation (default: false) 82 | - `enable_consumption` (optional): Show instant consumption characteristic (default: true) 83 | - `enable_total_consumption` (optional): Show total energy characteristic (default: true) 84 | - `enable_voltage` (optional): Show voltage characteristic (default: true) 85 | - `enable_ampere` (optional): Show current characteristic (default: true) 86 | - `negative_handling_mode` (optional): Handle negative values (0=zero, 1=absolute) 87 | - `debug_log` (optional): Enable debug logging (default: false) 88 | - `serial` (optional): Custom serial number (auto-generated if empty) 89 | 90 | ## Notes 91 | 92 | - **EVE App Required**: Energy characteristics only visible in EVE app, not native HomeKit 93 | - **Fixed Phase Support**: 3EM always uses 3 phases, EM always uses 2 phases 94 | - **Missing Phases Handled**: If phases are not physically connected, values are set to 0 95 | - **No Phase Configuration**: Phase count is automatically determined by device type 96 | - **Switch Function**: This plugin only handles energy monitoring, not switching 97 | - **Breaking Change**: v2.0.0 converted from accessory to platform plugin 98 | 99 | ## Troubleshooting 100 | 101 | 1. **Missing phases**: ✅ **Fixed!** Missing phases are automatically set to 0, no configuration needed 102 | 2. **Device not found**: Check IP address and network connectivity 103 | 3. **Authentication errors**: Verify username/password 104 | 4. **No data**: Ensure update_interval > timeout 105 | 5. **Multiple same names**: Use unique device names 106 | 6. **Wrong phase count**: Use correct device_type (3EM=3 phases, EM=2 phases) -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## 2.0.0 (2025-11-05) 4 | 5 | ### 🚨 BREAKING CHANGES 6 | - **Plugin Type Change**: Converted from accessory plugin to platform plugin to support multiple devices 7 | - **Configuration Update Required**: Existing configurations must be migrated from `accessories` to `platforms` section 8 | - **Removed Legacy Option**: `use_em` configuration option removed - use `device_type` instead 9 | 10 | ### ✨ New Features 11 | - **Multiple Device Support**: Configure and monitor multiple Shelly 3EM/EM devices simultaneously 12 | - **Fixed Phase Logic**: 3EM devices always use 3 phases, EM devices always use 2 phases (missing phases set to 0) 13 | - **Power Factor Support**: Optional power factor calculation for more accurate current measurement 14 | - **Characteristic Control**: Individual enable/disable options for consumption, voltage, current characteristics 15 | - **Auto-Generated Serial Numbers**: Unique serials generated automatically based on device name and IP 16 | - **Enhanced Device Identification**: Improved logging with device-specific prefixes for better debugging 17 | - **Better Configuration Validation**: Comprehensive validation of device configurations with helpful error messages 18 | - **Modernized Dependencies**: Updated from deprecated `request` to `node-fetch` for better performance 19 | 20 | ### 🛠️ Improvements 21 | - **Robust Phase Handling**: No more "insufficient emeters" errors - missing phases automatically handled 22 | - **Enhanced Error Handling**: Better error messages and validation for individual devices 23 | - **Improved Logging**: Device-specific logging prefixes for easier troubleshooting in multi-device setups 24 | - **Modern Code Architecture**: Complete code refactoring with async/await patterns 25 | - **Configuration Schema**: Enhanced UI schema with conditional fields and better validation 26 | - **UUID Generation**: Improved UUID generation for unique device identification 27 | - **Homebridge Config UI**: Better integration with configuration interface 28 | 29 | ### 📋 Migration Guide 30 | **Old Configuration (v1.x)**: 31 | ```json 32 | { 33 | "accessories": [ 34 | { 35 | "accessory": "3EMEnergyMeter", 36 | "name": "Energy Meter", 37 | "ip": "192.168.1.100" 38 | } 39 | ] 40 | } 41 | ``` 42 | 43 | **New Configuration (v2.0)**: 44 | ```json 45 | { 46 | "platforms": [ 47 | { 48 | "platform": "ShellyEnergyMeter", 49 | "name": "Energy Meters", 50 | "devices": [ 51 | { 52 | "name": "Main House Meter", 53 | "ip": "192.168.1.100", 54 | "device_type": "3EM" 55 | }, 56 | { 57 | "name": "Solar Meter", 58 | "ip": "192.168.1.101", 59 | "device_type": "EM", 60 | "use_pf": true, 61 | "enable_voltage": false, 62 | "debug_log": true 63 | } 64 | ] 65 | } 66 | ] 67 | } 68 | ``` 69 | 70 | ### 🔧 New Configuration Options 71 | - `device_type`: "3EM" (3 phases) or "EM" (2 phases) 72 | - `use_pf`: Enable power factor for current calculation 73 | - `enable_consumption`: Show/hide instant consumption characteristic 74 | - `enable_total_consumption`: Show/hide total energy characteristic 75 | - `enable_voltage`: Show/hide voltage characteristic 76 | - `enable_ampere`: Show/hide current characteristic 77 | - `negative_handling_mode`: Handle negative values (0=zero, 1=absolute) 78 | 79 | ### 🏗️ Technical Changes 80 | - **Dependency Updates**: Migrated from `request` to `node-fetch` (performance + security) 81 | - **Code Modernization**: Full refactoring with async/await patterns 82 | - **Phase Logic**: Fixed phase count per device type (no more flexible/broken phase detection) 83 | - **Error Handling**: Graceful handling of missing phases and network errors 84 | - **TypeScript Ready**: Better code structure for future TypeScript migration 85 | 86 | --- 87 | 88 | ## 1.1.4 (2025-10-03) 89 | 90 | ### Changes 91 | 92 | * Added support for Homebridge V2 93 | * Added a selection for what Energy Data is show 94 | * updated dependencies 95 | 96 | 97 | ## 1.1.3 (2021-05-18) 98 | 99 | ### Changes 100 | 101 | * Added a mode selection in order to specify what to do when negative values appear (Power returns etc.). 102 | 103 | 104 | ## 1.1.2 (2021-11-02) 105 | 106 | ### Changes 107 | 108 | * Added correct absolute ( abs() ) to calculations in order to comply to Homekit ranges (no negative values allowed). 109 | 110 | ## 1.1.1 (2021-11-02) 111 | 112 | ### Changes 113 | 114 | * Added absolute ( abs() ) to calculations in order to comply to Homekit ranges (no negative values allowed). 115 | 116 | ## 1.1.0 (2021-08-02) 117 | 118 | ### Changes 119 | 120 | * Added support for Shelly EM devices (beta) 121 | * Please set config flag use_em to true and 122 | use use_em_mode to get combined, channel1 or channel2 (setting 0,1,2) 123 | to use this plugin with a Shelly EM. 124 | 125 | ## 1.0.0 (2021-06-11) 126 | 127 | ### Changes 128 | 129 | * Bumped stable and tested release to major version 1.0.0 130 | * Just added donation button ;) 131 | 132 | ## 0.1.3 (2021-04-21) 133 | 134 | ### Changes 135 | 136 | * Added option to use the Power Factor (pf) when calculating Total Ampere. 137 | 138 | 139 | ## 0.1.2 (2021-04-10) 140 | 141 | ### Changes 142 | 143 | * Added returned metered values to debug log. 144 | 145 | -------------------------------------------------------------------------------- /config.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "pluginAlias": "ShellyEnergyMeter", 3 | "pluginType": "platform", 4 | "singular": false, 5 | "headerDisplay": "Shelly 3EM/EM Energy Meter Platform - Fixed Phase Support", 6 | "footerDisplay": "📱 Energy characteristics only visible in EVE app | 🔧 3EM uses 3 phases, EM uses 2 phases automatically", 7 | "schema": { 8 | "type": "object", 9 | "properties": { 10 | "name": { 11 | "title": "Platform Name", 12 | "type": "string", 13 | "default": "Shelly Energy Meters", 14 | "required": true 15 | }, 16 | "devices": { 17 | "title": "Energy Meter Devices", 18 | "type": "array", 19 | "items": { 20 | "type": "object", 21 | "properties": { 22 | "name": { 23 | "title": "Device Name", 24 | "type": "string", 25 | "required": true, 26 | "description": "Display name for this device" 27 | }, 28 | "ip": { 29 | "title": "IP Address", 30 | "type": "string", 31 | "required": true, 32 | "format": "ipv4", 33 | "description": "IP address of the Shelly device" 34 | }, 35 | "auth": { 36 | "title": "Authentication", 37 | "type": "object", 38 | "properties": { 39 | "user": { 40 | "title": "Username", 41 | "type": "string" 42 | }, 43 | "pass": { 44 | "title": "Password", 45 | "type": "string" 46 | } 47 | }, 48 | "description": "Optional authentication for protected devices" 49 | }, 50 | "timeout": { 51 | "title": "Timeout (ms)", 52 | "type": "integer", 53 | "default": 5000, 54 | "minimum": 1000, 55 | "maximum": 30000 56 | }, 57 | "update_interval": { 58 | "title": "Update Interval (ms)", 59 | "type": "integer", 60 | "default": 10000, 61 | "minimum": 5000, 62 | "maximum": 60000, 63 | "description": "Must be greater than timeout" 64 | }, 65 | "device_type": { 66 | "title": "📡 Device Type", 67 | "type": "string", 68 | "default": "3EM", 69 | "enum": ["3EM", "EM"], 70 | "description": "Shelly 3EM (3 phases) or Shelly EM (2 phases) - Missing phases will be set to 0" 71 | }, 72 | "use_pf": { 73 | "title": "Use Power Factor (pf) when calculating Total Ampere", 74 | "type": "boolean", 75 | "default": false, 76 | "required": true 77 | }, 78 | "enable_consumption": { 79 | "title": "Enable Instant Consumption Characteristic", 80 | "type": "boolean", 81 | "default": true, 82 | "required": true, 83 | "description": "Disable to hide the Consumption characteristic." 84 | }, 85 | "enable_total_consumption": { 86 | "title": "Enable Total Consumption Characteristic", 87 | "type": "boolean", 88 | "default": true, 89 | "required": true, 90 | "description": "Disable to hide the Total Energy characteristic." 91 | }, 92 | "enable_voltage": { 93 | "title": "Enable Voltage Characteristic", 94 | "type": "boolean", 95 | "default": true, 96 | "required": true, 97 | "description": "Disable to hide the Voltage characteristic." 98 | }, 99 | "enable_ampere": { 100 | "title": "Enable Current Characteristic", 101 | "type": "boolean", 102 | "default": true, 103 | "required": true, 104 | "description": "Disable to hide the Current characteristic." 105 | }, 106 | "use_em_mode": { 107 | "title": "📊 EM Channel Mode", 108 | "type": "integer", 109 | "default": 0, 110 | "minimum": 0, 111 | "maximum": 2, 112 | "description": "0=combine all channels, 1=channel 1 only, 2=channel 2 only (EM only)", 113 | "condition": { 114 | "functionBody": "return model.device_type === 'EM';" 115 | } 116 | }, 117 | "negative_handling_mode": { 118 | "title": "Negative Values", 119 | "type": "integer", 120 | "default": 0, 121 | "minimum": 0, 122 | "maximum": 1, 123 | "description": "0=set to zero, 1=show absolute value" 124 | }, 125 | "debug_log": { 126 | "title": "Debug Logging", 127 | "type": "boolean", 128 | "default": false, 129 | "description": "Enable detailed logging for this device" 130 | }, 131 | "serial": { 132 | "title": "Serial Number", 133 | "type": "string", 134 | "description": "Custom serial (auto-generated if empty)" 135 | } 136 | }, 137 | "required": ["name", "ip"] 138 | } 139 | } 140 | }, 141 | "required": ["name", "devices"] 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const fetch = require('node-fetch'); 2 | const fakegatoHistory = require('fakegato-history'); 3 | const { version } = require('./package.json'); 4 | 5 | let Service, Characteristic, PlatformAccessory, FakeGatoHistoryService; 6 | const POWER_METER_SERVICE_UUID = '00000001-0000-1777-8000-775D67EC4377'; 7 | 8 | module.exports = function (homebridge) { 9 | Service = homebridge.hap.Service; 10 | Characteristic = homebridge.hap.Characteristic; 11 | PlatformAccessory = homebridge.platformAccessory; 12 | 13 | // Register Eve characteristics 14 | registerEveCharacteristics(); 15 | 16 | // Initialize FakeGato with the homebridge instance 17 | FakeGatoHistoryService = fakegatoHistory(homebridge); 18 | 19 | homebridge.registerPlatform("homebridge-shelly-energy-meter", "ShellyEnergyMeter", EnergyMeterPlatform); 20 | } 21 | 22 | function registerEveCharacteristics() { 23 | // Eve Power Consumption (Watts) 24 | global.EvePowerConsumption = class extends Characteristic { 25 | constructor() { 26 | super('Consumption', 'E863F10D-079E-48FF-8F27-9C2605A29F52'); 27 | this.setProps({ 28 | format: Characteristic.Formats.UINT16, 29 | unit: "Watts", 30 | maxValue: 100000, 31 | minValue: 0, 32 | minStep: 1, 33 | perms: [Characteristic.Perms.READ, Characteristic.Perms.NOTIFY] 34 | }); 35 | this.value = this.getDefaultValue(); 36 | } 37 | }; 38 | global.EvePowerConsumption.UUID = 'E863F10D-079E-48FF-8F27-9C2605A29F52'; 39 | 40 | // Eve Total Consumption (kWh) 41 | global.EveTotalConsumption = class extends Characteristic { 42 | constructor() { 43 | super('Energy', 'E863F10C-079E-48FF-8F27-9C2605A29F52'); 44 | this.setProps({ 45 | format: Characteristic.Formats.FLOAT, 46 | unit: 'kWh', 47 | maxValue: 1000000000, 48 | minValue: 0, 49 | minStep: 0.001, 50 | perms: [Characteristic.Perms.READ, Characteristic.Perms.NOTIFY] 51 | }); 52 | this.value = this.getDefaultValue(); 53 | } 54 | }; 55 | global.EveTotalConsumption.UUID = 'E863F10C-079E-48FF-8F27-9C2605A29F52'; 56 | 57 | // Eve Voltage 58 | global.EveVoltage = class extends Characteristic { 59 | constructor() { 60 | super('Volt', 'E863F10A-079E-48FF-8F27-9C2605A29F52'); 61 | this.setProps({ 62 | format: Characteristic.Formats.FLOAT, 63 | unit: 'Volt', 64 | maxValue: 1000000000, 65 | minValue: 0, 66 | minStep: 0.001, 67 | perms: [Characteristic.Perms.READ, Characteristic.Perms.NOTIFY] 68 | }); 69 | this.value = this.getDefaultValue(); 70 | } 71 | }; 72 | global.EveVoltage.UUID = 'E863F10A-079E-48FF-8F27-9C2605A29F52'; 73 | 74 | // Eve Current 75 | global.EveAmpere = class extends Characteristic { 76 | constructor() { 77 | super('Ampere', 'E863F126-079E-48FF-8F27-9C2605A29F52'); 78 | this.setProps({ 79 | format: Characteristic.Formats.FLOAT, 80 | unit: 'Ampere', 81 | maxValue: 1000000000, 82 | minValue: 0, 83 | minStep: 0.001, 84 | perms: [Characteristic.Perms.READ, Characteristic.Perms.NOTIFY] 85 | }); 86 | this.value = this.getDefaultValue(); 87 | } 88 | }; 89 | global.EveAmpere.UUID = 'E863F126-079E-48FF-8F27-9C2605A29F52'; 90 | 91 | // Eve Power Meter Service 92 | global.PowerMeterService = class extends Service { 93 | constructor(displayName, subtype) { 94 | super(displayName, POWER_METER_SERVICE_UUID, subtype); 95 | this.addOptionalCharacteristic(global.EvePowerConsumption); 96 | this.addOptionalCharacteristic(global.EveTotalConsumption); 97 | this.addOptionalCharacteristic(global.EveVoltage); 98 | this.addOptionalCharacteristic(global.EveAmpere); 99 | } 100 | }; 101 | global.PowerMeterService.UUID = POWER_METER_SERVICE_UUID; 102 | } 103 | 104 | function EnergyMeterPlatform(log, config, api) { 105 | this.log = log; 106 | this.config = config || {}; 107 | this.api = api; 108 | this.accessories = []; 109 | 110 | if (!this.config.devices || !Array.isArray(this.config.devices)) { 111 | this.log.warn('No devices configured or invalid configuration'); 112 | return; 113 | } 114 | 115 | if (api) { 116 | api.on('didFinishLaunching', () => this.discoverDevices()); 117 | } 118 | } 119 | 120 | EnergyMeterPlatform.prototype.configureAccessory = function(accessory) { 121 | this.accessories.push(accessory); 122 | } 123 | 124 | EnergyMeterPlatform.prototype.discoverDevices = function() { 125 | for (let i = 0; i < this.config.devices.length; i++) { 126 | const device = this.config.devices[i]; 127 | 128 | // Validate device config 129 | if (!device.name || !device.ip) { 130 | this.log.error(`Device ${i + 1} missing name or ip`); 131 | continue; 132 | } 133 | 134 | const uuid = this.api.hap.uuid.generate(`${device.name}-${device.ip}`); 135 | const existingAccessory = this.accessories.find(acc => acc.UUID === uuid); 136 | 137 | if (existingAccessory) { 138 | this.log.info(`Updating existing accessory: ${device.name}`); 139 | existingAccessory.context.device = device; 140 | new EnergyMeter(this.log, device, existingAccessory, this.api); 141 | this.api.updatePlatformAccessories([existingAccessory]); 142 | } else { 143 | this.log.info(`Adding new accessory: ${device.name}`); 144 | const accessory = new PlatformAccessory(device.name, uuid); 145 | accessory.context.device = device; 146 | new EnergyMeter(this.log, device, accessory, this.api); 147 | this.api.registerPlatformAccessories('homebridge-shelly-energy-meter', 'ShellyEnergyMeter', [accessory]); 148 | this.accessories.push(accessory); 149 | } 150 | } 151 | 152 | // Remove obsolete accessories 153 | const currentUUIDs = this.config.devices.map((device, i) => 154 | this.api.hap.uuid.generate(`${device.name}-${device.ip}`) 155 | ); 156 | const obsoleteAccessories = this.accessories.filter(acc => !currentUUIDs.includes(acc.UUID)); 157 | 158 | if (obsoleteAccessories.length > 0) { 159 | this.log.info(`Removing ${obsoleteAccessories.length} obsolete accessory(ies)`); 160 | this.api.unregisterPlatformAccessories('homebridge-shelly-energy-meter', 'ShellyEnergyMeter', obsoleteAccessories); 161 | this.accessories = this.accessories.filter(acc => currentUUIDs.includes(acc.UUID)); 162 | } 163 | } 164 | 165 | function EnergyMeter(log, config, accessory, api) { 166 | this.log = log; 167 | this.config = config; 168 | this.accessory = accessory; 169 | this.api = api; 170 | this.name = config.name; 171 | this.ip = config.ip; 172 | this.updateInterval = config.update_interval || 10000; 173 | this.timeout = config.timeout || 5000; 174 | 175 | // Device type determination 176 | this.deviceType = config.device_type || '3EM'; // Default to 3EM if not specified 177 | 178 | // Fixed phase configuration based on device type 179 | this.connectedPhases = this.deviceType === 'EM' ? 2 : 3; 180 | this.enabledPhases = Array.from({length: this.connectedPhases}, (_, i) => i + 1); 181 | 182 | if (this.debugLog) { 183 | this.log.info(`[${this.name}] Device: ${this.deviceType}, Fixed phases: ${this.connectedPhases} (${this.enabledPhases.join(', ')})`); 184 | } 185 | 186 | // EM specific configuration 187 | this.emMode = config.use_em_mode || 0; 188 | 189 | this.auth = config.auth; 190 | this.debugLog = config.debug_log || false; 191 | 192 | // New configuration options 193 | this.usePowerFactor = config.use_pf || false; 194 | this.enableConsumption = config.enable_consumption !== false; // Default true 195 | this.enableTotalConsumption = config.enable_total_consumption !== false; // Default true 196 | this.enableVoltage = config.enable_voltage !== false; // Default true 197 | this.enableAmpere = config.enable_ampere !== false; // Default true 198 | 199 | // Current values 200 | this.power = 0; 201 | this.totalEnergy = 0; 202 | this.voltage = 0; 203 | this.current = 0; 204 | 205 | if (this.debugLog) { 206 | this.log.info(`[${this.name}] Initialized ${this.deviceType} with ${this.connectedPhases} connected phase(s)`); 207 | } 208 | 209 | this.setupServices(); 210 | this.startUpdating(); 211 | } 212 | 213 | EnergyMeter.prototype.setupServices = function() { 214 | // Information Service 215 | const informationService = this.accessory.getService(Service.AccessoryInformation) || 216 | this.accessory.addService(Service.AccessoryInformation); 217 | 218 | informationService 219 | .setCharacteristic(Characteristic.Manufacturer, "Shelly") 220 | .setCharacteristic(Characteristic.Model, `Shelly ${this.deviceType}`) 221 | .setCharacteristic(Characteristic.SerialNumber, this.config.serial || `${this.deviceType}-${this.ip.replace(/\./g, '')}`) 222 | .setCharacteristic(Characteristic.FirmwareRevision, version); 223 | 224 | // Power Meter Service - use device IP as unique subtype for persistence 225 | const serviceSubtype = this.ip; 226 | const existingService = (this.accessory.services || []).find(service => 227 | service && service.UUID === POWER_METER_SERVICE_UUID && service.subtype === serviceSubtype 228 | ); 229 | 230 | if (existingService) { 231 | this.service = existingService; 232 | // Ensure name stays up to date when accessory name changes 233 | this.service.displayName = this.name; 234 | this.service.setCharacteristic(Characteristic.Name, this.name); 235 | if (this.debugLog) { 236 | this.log.info(`[${this.name}] Reusing existing PowerMeter service with subtype: ${serviceSubtype}`); 237 | } 238 | } else { 239 | this.service = this.accessory.addService(global.PowerMeterService, this.name, serviceSubtype); 240 | if (this.debugLog) { 241 | this.log.info(`[${this.name}] Created new PowerMeter service with subtype: ${serviceSubtype}`); 242 | } 243 | } 244 | 245 | // Clean up duplicate characteristics that may linger from previous versions 246 | this.removeDuplicateCharacteristics(); 247 | 248 | // Clean up characteristics that are no longer needed 249 | if (!this.enableConsumption) { 250 | this.removeCharacteristicIfExists(global.EvePowerConsumption); 251 | } 252 | if (!this.enableTotalConsumption) { 253 | this.removeCharacteristicIfExists(global.EveTotalConsumption); 254 | } 255 | if (!this.enableVoltage) { 256 | this.removeCharacteristicIfExists(global.EveVoltage); 257 | } 258 | if (!this.enableAmpere) { 259 | this.removeCharacteristicIfExists(global.EveAmpere); 260 | } 261 | 262 | // Add characteristics based on configuration 263 | if (this.enableConsumption) { 264 | this.powerChar = this.ensureCharacteristic(global.EvePowerConsumption); 265 | this.powerChar.removeAllListeners('get'); 266 | this.powerChar.on('get', callback => callback(null, this.power)); 267 | } 268 | 269 | if (this.enableTotalConsumption) { 270 | this.totalEnergyChar = this.ensureCharacteristic(global.EveTotalConsumption); 271 | this.totalEnergyChar.removeAllListeners('get'); 272 | this.totalEnergyChar.on('get', callback => callback(null, this.totalEnergy)); 273 | } 274 | 275 | if (this.enableVoltage) { 276 | this.voltageChar = this.ensureCharacteristic(global.EveVoltage); 277 | this.voltageChar.removeAllListeners('get'); 278 | this.voltageChar.on('get', callback => callback(null, this.voltage)); 279 | } 280 | 281 | if (this.enableAmpere) { 282 | this.currentChar = this.ensureCharacteristic(global.EveAmpere); 283 | this.currentChar.removeAllListeners('get'); 284 | this.currentChar.on('get', callback => callback(null, this.current)); 285 | } 286 | 287 | // Initialize history service for Eve app 288 | this.setupHistory(); 289 | } 290 | 291 | EnergyMeter.prototype.ensureCharacteristic = function(characteristicClass) { 292 | const uuid = characteristicClass.UUID; 293 | let characteristic = this.service.characteristics.find(char => char.UUID === uuid); 294 | if (!characteristic) { 295 | characteristic = this.service.addCharacteristic(characteristicClass); 296 | } 297 | return characteristic; 298 | }; 299 | 300 | EnergyMeter.prototype.removeCharacteristicIfExists = function(characteristicClass) { 301 | const uuid = characteristicClass.UUID; 302 | const matches = this.service.characteristics.filter(char => char.UUID === uuid); 303 | matches.forEach(char => this.service.removeCharacteristic(char)); 304 | }; 305 | 306 | EnergyMeter.prototype.removeDuplicateCharacteristics = function() { 307 | const seen = new Map(); 308 | // clone array because we mutate characteristics during iteration 309 | for (const char of [...this.service.characteristics]) { 310 | if (!seen.has(char.UUID)) { 311 | seen.set(char.UUID, char); 312 | continue; 313 | } 314 | this.service.removeCharacteristic(char); 315 | if (this.debugLog) { 316 | this.log.info(`[${this.name}] Removed duplicate characteristic with UUID ${char.UUID}`); 317 | } 318 | } 319 | }; 320 | 321 | EnergyMeter.prototype.setupHistory = function() { 322 | if (!FakeGatoHistoryService) { 323 | this.log.warn(`[${this.name}] History service unavailable - FakeGato not initialized`); 324 | return; 325 | } 326 | // Initialize history service when first needed 327 | if (this.historyService === undefined) { 328 | try { 329 | // Use setTimeout to delay initialization until all APIs are loaded 330 | setTimeout(() => { 331 | try { 332 | this.historyService = new FakeGatoHistoryService("energy", this.accessory, { 333 | storage: 'fs', 334 | log: this.log 335 | }); 336 | if (this.debugLog) { 337 | this.log.info(`[${this.name}] History service initialized successfully`); 338 | } 339 | } catch (error) { 340 | this.log.warn(`[${this.name}] Failed to initialize history service: ${error.message}`); 341 | this.historyService = null; 342 | } 343 | }, 1000); // 1 second delay 344 | 345 | // Set to null temporarily to prevent multiple attempts 346 | this.historyService = null; 347 | } catch (error) { 348 | this.log.warn(`[${this.name}] Failed to setup history service: ${error.message}`); 349 | this.historyService = null; 350 | } 351 | } 352 | } 353 | 354 | EnergyMeter.prototype.startUpdating = function() { 355 | if (this.updateInterval > 0) { 356 | setInterval(() => this.updateValues(), this.updateInterval); 357 | this.updateValues(); // Initial update 358 | } 359 | } 360 | 361 | EnergyMeter.prototype.updateValues = async function() { 362 | try { 363 | const url = `http://${this.ip}/status/emeters`; 364 | const options = { 365 | method: 'GET', 366 | timeout: this.timeout 367 | }; 368 | 369 | if (this.auth) { 370 | const auth = Buffer.from(`${this.auth.user}:${this.auth.pass}`).toString('base64'); 371 | options.headers = { 'Authorization': `Basic ${auth}` }; 372 | } 373 | 374 | const response = await fetch(url, options); 375 | if (!response.ok) { 376 | throw new Error(`HTTP ${response.status}: ${response.statusText}`); 377 | } 378 | 379 | const data = await response.json(); 380 | this.processData(data); 381 | 382 | } catch (error) { 383 | this.log.error(`[${this.name}] Update failed: ${error.message}`); 384 | } 385 | } 386 | 387 | EnergyMeter.prototype.processData = function(data) { 388 | if (!data.emeters || !Array.isArray(data.emeters)) { 389 | this.log.error(`[${this.name}] Invalid data format`); 390 | return; 391 | } 392 | 393 | const emeters = data.emeters; 394 | const availablePhases = emeters.length; 395 | 396 | // Always use fixed phase count, missing phases will be 0 397 | let power = 0, totalEnergy = 0, voltage = 0, current = 0; 398 | let validPhases = 0; 399 | 400 | if (this.deviceType === 'EM') { 401 | // Shelly EM processing - always process 2 phases 402 | switch (this.emMode) { 403 | case 1: // Channel 1 only 404 | const phase1 = emeters[0] || {}; 405 | power = this.safeFloat(phase1.power); 406 | totalEnergy = this.safeFloat(phase1.total) / 1000; 407 | voltage = this.safeFloat(phase1.voltage); 408 | const pf1 = this.safeFloat(phase1.pf) || 1.0; 409 | current = this.calculateCurrent(power, voltage, pf1); 410 | validPhases = 1; 411 | break; 412 | case 2: // Channel 2 only 413 | const phase2 = emeters[1] || {}; 414 | power = this.safeFloat(phase2.power); 415 | totalEnergy = this.safeFloat(phase2.total) / 1000; 416 | voltage = this.safeFloat(phase2.voltage); 417 | const pf2 = this.safeFloat(phase2.pf) || 1.0; 418 | current = this.calculateCurrent(power, voltage, pf2); 419 | validPhases = 1; 420 | break; 421 | default: // Combined channels - always process both phases 422 | let totalPowerFactor = 0; 423 | for (let i = 0; i < 2; i++) { // Always 2 phases for EM 424 | const phase = emeters[i] || {}; // Use empty object if phase missing 425 | power += this.safeFloat(phase.power); 426 | totalEnergy += this.safeFloat(phase.total); 427 | voltage += this.safeFloat(phase.voltage); 428 | totalPowerFactor += this.safeFloat(phase.pf) || 1.0; 429 | validPhases++; 430 | } 431 | if (validPhases > 0) { 432 | totalEnergy = totalEnergy / 1000; 433 | voltage = voltage / validPhases; // Average voltage 434 | const avgPowerFactor = totalPowerFactor / validPhases; // Average power factor 435 | current = this.calculateCurrent(power, voltage, avgPowerFactor); 436 | } 437 | } 438 | } else { 439 | // Shelly 3EM processing - always process 3 phases 440 | for (let i = 0; i < 3; i++) { // Always 3 phases for 3EM 441 | const phase = emeters[i] || {}; // Use empty object if phase missing 442 | power += this.safeFloat(phase.power); 443 | totalEnergy += this.safeFloat(phase.total); 444 | voltage += this.safeFloat(phase.voltage); 445 | current += this.safeFloat(phase.current); 446 | validPhases++; 447 | } 448 | // Calculate averages where appropriate 449 | if (validPhases > 0) { 450 | totalEnergy = totalEnergy / 1000; // Convert to kWh 451 | voltage = voltage / validPhases; // Average voltage 452 | // Power and current are already summed 453 | } 454 | } 455 | 456 | // Handle negative values 457 | if (this.config.negative_handling_mode === 1) { 458 | power = Math.abs(power); 459 | totalEnergy = Math.abs(totalEnergy); 460 | voltage = Math.abs(voltage); 461 | current = Math.abs(current); 462 | } else { 463 | power = Math.max(0, power); 464 | totalEnergy = Math.max(0, totalEnergy); 465 | voltage = Math.max(0, voltage); 466 | current = Math.max(0, current); 467 | } 468 | 469 | // Update values 470 | this.power = power; 471 | this.totalEnergy = totalEnergy; 472 | this.voltage = voltage; 473 | this.current = current; 474 | 475 | // Update characteristics (only if enabled) 476 | if (this.powerChar) this.powerChar.updateValue(this.power); 477 | if (this.totalEnergyChar) this.totalEnergyChar.updateValue(this.totalEnergy); 478 | if (this.voltageChar) this.voltageChar.updateValue(this.voltage); 479 | if (this.currentChar) this.currentChar.updateValue(this.current); 480 | 481 | // Add to history (if available) 482 | if (this.historyService) { 483 | this.historyService.addEntry({ 484 | time: Math.round(Date.now() / 1000), 485 | power: this.power 486 | }); 487 | } 488 | 489 | if (this.debugLog) { 490 | this.log.info(`[${this.name}] Updated (${validPhases} phases): ${this.power}W, ${this.totalEnergy}kWh, ${this.voltage}V, ${this.current}A`); 491 | } 492 | } 493 | 494 | EnergyMeter.prototype.safeFloat = function(value) { 495 | const parsed = parseFloat(value); 496 | return isNaN(parsed) ? 0 : parsed; 497 | } 498 | 499 | EnergyMeter.prototype.calculateCurrent = function(power, voltage, powerFactor = 1.0) { 500 | if (voltage <= 0) return 0; 501 | 502 | if (this.usePowerFactor && powerFactor > 0) { 503 | // I = P / (V * pf) - More accurate for AC with reactive loads 504 | return power / (voltage * powerFactor); 505 | } else { 506 | // I = P / V - Simple calculation (resistive loads only) 507 | return power / voltage; 508 | } 509 | } --------------------------------------------------------------------------------