2025-08-14 22:41:23 +02:00
exports . id = 'socomec' ;
exports . title = 'Energomonitor socomec' ;
exports . version = '1.0.3' ;
exports . group = 'Worksys' ;
exports . color = '#2134B0' ;
exports . output = [ "red" , "white" , "yellow" ] ;
exports . click = false ;
exports . author = 'Rastislav Kovac' ;
exports . icon = 'bolt' ;
2025-08-14 22:48:26 +02:00
2025-08-14 22:41:23 +02:00
exports . readme = ` Modbus requests to energomonitor socomec suitcase ` ;
const streamBuilder = require ( "./helper/energo_streambuilder" ) ;
const structure = {
2025-08-14 22:48:26 +02:00
"192.168.1.21" : {
16 : "i-35" ,
"2A" : "i-60A" ,
"2B" : "i-60B" ,
"3A" : "i-60A" ,
"3B" : "i-60B" ,
"4A" : "i-60A" ,
"4B" : "i-60B" ,
"5A" : "i-60A" ,
"5B" : "i-60B" ,
2025-08-14 22:41:23 +02:00
"6A" : "i-60A" ,
"6B" : "i-60B" ,
"7A" : "i-60A" ,
"7B" : "i-60B" ,
"8A" : "i-60A" ,
"8B" : "i-60B" ,
2025-08-14 22:48:26 +02:00
"9A" : "i-60A" ,
"9B" : "i-60B" ,
2025-08-14 22:41:23 +02:00
"10A" : "i-60A" ,
"10B" : "i-60B" ,
"11A" : "i-60A" ,
"11B" : "i-60B" ,
"12A" : "i-60A" ,
"12B" : "i-60B" ,
"13A" : "i-60A" ,
"13B" : "i-60B" ,
"14A" : "i-60A" ,
"14B" : "i-60B" ,
} ,
2025-08-14 22:48:26 +02:00
"192.168.1.22" : {
10 : "i-35" ,
2025-08-14 22:41:23 +02:00
"2A" : "i-60A" ,
"2B" : "i-60B" ,
"3A" : "i-60A" ,
"3B" : "i-60B" ,
"4A" : "i-60A" ,
"4B" : "i-60B" ,
"5A" : "i-60A" ,
"5B" : "i-60B" ,
"6A" : "i-60A" ,
"6B" : "i-60B" ,
"7A" : "i-60A" ,
"7B" : "i-60B" ,
"8A" : "i-60A" ,
"8B" : "i-60B" ,
"9A" : "i-60A" ,
2025-08-14 22:48:26 +02:00
"9B" : "i-60B"
2025-08-14 22:41:23 +02:00
}
} ;
exports . install = function ( instance ) {
const modbus = require ( 'jsmodbus' ) ;
const net = require ( 'net' ) ;
require ( 'events' ) . EventEmitter . defaultMaxListeners = 20 ;
2025-08-14 22:48:26 +02:00
const allModulesEnergy = { unipi _75 : "" } ; // key to identify source data
2025-08-14 22:41:23 +02:00
const date = new Date ( ) ;
let hour = date . getHours ( ) ;
let sendAllModulesEnergy = setInterval ( ( ) => {
const d = new Date ( ) ;
const h = d . getHours ( ) ;
if ( h != hour )
{
instance . send ( 2 , allModulesEnergy ) ;
hour = h ;
}
} , 180000 ) ;
instance . on ( 'close' , function ( ) {
clearInterval ( sendAllModulesEnergy ) ;
} ) ;
const conversionTable = streamBuilder . makeStreamsTable ( structure ) ;
// const conversionTable =
// {
2025-08-14 22:48:26 +02:00
// "192.168.1.21": {
2025-08-14 22:41:23 +02:00
// "streams": [
// {
// "unitId": 1,
// "section": "",
// "name": 18488,
2025-08-14 22:48:26 +02:00
// "tb_value":"total_energy",
2025-08-14 22:41:23 +02:00
// "bytes": 2,
// "multiplier":1,
2025-08-14 22:48:26 +02:00
// "previousEnergy": null,
// "month": 2
2025-08-14 22:41:23 +02:00
// },
// ]
// }
// }
const tbNames = {
2025-08-14 22:48:26 +02:00
"192.168.1.21" : {
"16" : "aw4eELG2DlPMdn1JW0Bz2Z0qQXOZRN3xB5yp8VKr" ,
"2A" : "d9x2V5LGYBzXp4mMRAOPV10PloaqJwnQj6DgrNe3" ,
"2B" : "eod9aRWLVl34Gx1Dn7VYbDk2rz6qjgmpEXwQJN5Z" ,
"3A" : "3a5oqJN1bgnx4Ol9dk8BnqAByE6jQ8mKDWMpGrLV" ,
"3B" : "EjgWGnXaLy9opPOz20ngyQk86BlYM3w1deVQvbKr" ,
"4A" : "rDbQ84xzwgdqEoPm3kbPKDA9anOZY1RXyBv2LVM6" ,
"4B" : "wvKJdZML6mXP4DzWBAXOj87jxNloa5g23Ve9Y1ry" ,
"5A" : "E6Kg9oDnLWyzPRMva7vWy9AJxp4VG58qO2w1lZYe" ,
"5B" : "roKgWqY95V3mXMRzyAjrynAbLjexpJPvaGDBw826" ,
"6A" : "Nzp2OoJlqn6r1ZgvdA3RoE7abBwP5G4eE3RQmyxD" ,
"6B" : "rDbQ84xzwgdqEoPm3kbPKWA9anOZY1RXyBv2LVM6" ,
"7A" : "E6Kg9oDnLWyzPRMva7vWyyAJxp4VG58qO2w1lZYe" ,
"7B" : "roKgWqY95V3mXMRzyAjry6AbLjexpJPvaGDBw826" ,
"8A" : "nJL5lPMwBx23YpqRe0rqy4AdamXvWVbOrD4gNzy8" ,
"8B" : "ZmRwd93QL4gaezxEbAx1yb01prn2XjlPvGyqJ6BO" ,
"9A" : "eod9aRWLVl34Gx1Dn7VYb9k2rz6qjgmpEXwQJN5Z" ,
"9B" : "Nzp2OoJlqn6r1ZgvdA3Rov7abBwP5G4eE3RQmyxD" ,
"10A" : "3a5oqJN1bgnx4Ol9dk8Bn1AByE6jQ8mKDWMpGrLV" ,
"10B" : "EjgWGnXaLy9opPOz20ngydk86BlYM3w1deVQvbKr" ,
"11A" : "PLBJzmK1r3Gynd6OW0g2yqAe5wV4vx9bDEqNgYR8" ,
"11B" : "wvKJdZML6mXP4DzWBAXOjV7jxNloa5g23Ve9Y1ry" ,
"12A" : "nJL5lPMwBx23YpqRe0rqybAdamXvWVbOrD4gNzy8" ,
"12B" : "52dD6ZlV1QaOpRBmbAqvyN0KnGzWMLj4eJq38Pgo" ,
"13A" : "XMBbew5z4ELrZa2mRAdZ4xk8vPN6gy3DdVYlpKjq" ,
"13B" : "PLBJzmK1r3Gynd6OW0g2yzAe5wV4vx9bDEqNgYR8" ,
"14A" : "52dD6ZlV1QaOpRBmbAqvyb0KnGzWMLj4eJq38Pgo" ,
"14B" : "gYbDLqlyZVoRerQpB72Gp4AWJnwM5z24POKa8Exj" ,
} ,
"192.168.1.22" : {
"10" : "ZmRwd93QL4gaezxEbAx1yw01prn2XjlPvGyqJ6BO" ,
"2A" : "B5EoxeMVp4zwr8nqW0GeGG7RjvD1PNamOGbLg63Z" ,
"2B" : "WlVJBygjDZMeKX3vnAMW6Pk8NqdmG2x1Y69LQ4P5" ,
"3A" : "zrR51V2ajQ9ZLygPKkEPz10YDq38xOJolENBXGnv" ,
"3B" : "BaY3Xpy1EbKGjLq2O7m9N97rx8owgQz9P4dDJRmN" ,
"4A" : "JzwxZXOvDj1bVrN4nkWe3zA8qdyBl3MRKLpGPgaQ" ,
"4B" : "o9vbeQlLMVg8j5dq4kedZK0NxZpEmnXzwYKO1ar2" ,
"5A" : "gP1eOZVj3Q9lv5aDEk4M4a7rdpqW8yLm2BbKzJxM" ,
"5B" : "5dBNwRp9graYJxZn409RBvklVov1b2QLPDqGm6XK" ,
"6A" : "g9OxBZ5KRwNznlY6pApbNOkWXvjdEL4eGQobMDy2" ,
"6B" : "pE5X8NQPaow6vlOZxk6Y6z0q42ezGBMyWgDVjR3L" ,
"7A" : "2O14VBzl8aDmWdNw3A535GkGyZ5qLJoEMpj6R9ng" ,
"7B" : "DbQY6zyveZRwK5drV0ZlB87joE4XJM83N9xl2nWq" ,
"8A" : "6lQGaY9RDywdVzObj0PZpb7Pg4NBn3exEK51LWZq" ,
"8B" : "m6EYyZoJ4gWexdjVPARaxV7RDOq9wv2N5XzKGplr" ,
"9A" : "apKVJBwOyrP35m2lv7KEpz0YXbeWNd64En9GxRqg" ,
"9B" : "OzNMgZ9n43qPbjXmy7zONeA2DKdYvW5e6pxGRrVa"
2025-08-14 22:41:23 +02:00
}
} ;
2025-08-14 22:48:26 +02:00
// static class attribute ???
const valuesToRecount = [ 'total_energy' , 'phase_1_current' , 'phase_2_current' , 'phase_3_current' , 'neutral_wire_current' , 'total_active_power' , 'total_reactive_power' , 'total_apparent_power' ] ;
2025-08-14 22:41:23 +02:00
class SocketWithClients {
constructor ( ip , data ) {
this . ip = ip ;
this . data = data ;
this . options = {
'host' : this . ip ,
'port' : '502'
}
this . streams = data . streams ; //pole
2025-08-14 22:48:26 +02:00
// distribute ==> we are sending attributes from 'valuesToRecount' with value divided by 2 to these 4 tbNames. To ensure, we send 'energy_last_month' and 'energy_update' attribute as well, we must store 'month' key and previousEnergy
// can be static class attribute ??
this . distribute = {
"6lQGaY9RDywdVzObj0PZpb7Pg4NBn3exEK51LWZq" : { month : this . getCurrentMonth ( ) , previousEnergy : null , tbNames : [ "nJL5lPMwBx23YpqRe0rq4bAdamXvWVbOrD4gNzy8" , "ZmRwd93QL4gaezxEbAx1oK01prn2XjlPvGyqJ6BO" ] } ,
"PLBJzmK1r3Gynd6OW0g2yqAe5wV4vx9bDEqNgYR8" : { month : this . getCurrentMonth ( ) , previousEnergy : null , tbNames : [ "E6Kg9oDnLWyzPRMva7vWR9AJxp4VG58qO2w1lZYe" , "roKgWqY95V3mXMRzyAjrYnAbLjexpJPvaGDBw826" ] }
}
2025-08-14 22:41:23 +02:00
this . startSocket ( ) ;
}
2025-08-14 22:48:26 +02:00
getCurrentMonth = ( ) => {
const date = new Date ( ) ;
return date . getMonth ( ) ;
} ;
2025-08-14 22:41:23 +02:00
startSocket = ( ) => {
let obj = this ;
this . index = 0 ;
this . clients = { } ;
this . socket = new net . Socket ( ) ;
this . socket . connect ( this . options , function ( ) {
console . log ( 'Connected to socket server' ) ;
} ) ;
this . socket . on ( 'error' , function ( e ) {
console . log ( 'socket connection error' , e ) ;
if ( e . code == 'ECONNREFUSED' || e . code == 'ECONNRESET' ) {
console . log ( exports . title + ' Waiting 1 minute before trying to connect again' ) ;
setTimeout ( obj . startSocket , 60000 ) ;
}
} ) ;
this . socket . on ( 'close' , function ( ) {
console . log ( 'Socket connection closed ' + exports . title + ' Waiting 1 minute before trying to connect again' ) ;
setTimeout ( obj . startSocket , 60000 ) ;
} ) ;
// we create client for all modules (unitIds) and push them into dictionary
for ( let i = 0 ; i < obj . data . streams . length ; i ++ )
{
if ( ! this . clients . hasOwnProperty ( obj . data . streams [ i ] . unitId ) )
{
this . clients [ obj . data . streams [ i ] . unitId ] = new modbus . client . TCP ( this . socket , obj . data . streams [ i ] . unitId ) ;
}
}
this . socket . on ( 'connect' , function ( ) {
console . log ( "socket connected" ) ;
setTimeout ( obj . readRegisters , 10000 ) ;
} ) ;
} ;
readRegisters = ( ) => {
const lenghtOfStreams = this . streams . length ;
if ( this . index >= lenghtOfStreams )
{
this . index = 0 ;
setTimeout ( this . readRegisters , 300000 ) ;
return ;
}
let unitId = this . streams [ this . index ] . unitId ;
let section = this . streams [ this . index ] . section ;
let register = this . streams [ this . index ] . name ;
let bytes = this . streams [ this . index ] . bytes ;
let date = Date . now ( ) ;
let tbval = this . streams [ this . index ] . tb _value ;
// console.log("citam tieto hodnoty",unitId, register, tbval);
let obj = this ;
this . clients [ unitId ] . readHoldingRegisters ( register , bytes )
. then ( function ( resp ) {
resp = resp . response . _body . valuesAsArray ;
// console.log(unitId, register, tbval, resp);
obj . sendData ( resp , register , date , unitId , section ) ;
obj . index ++ ;
setTimeout ( obj . readRegisters , 0 ) ;
} ) . catch ( function ( ) {
console . log ( "error pri citani z grafie" , register , unitId , section , tbNames [ obj . ip ] [ unitId + section ] , tbval ) ;
//! IMPLEMENTOVAT POSIELANIE CHYB PODLA POSLEDNHO REGISTRA V MODULE === "total_power_factor"
if ( tbval === "total_power_factor" )
{
obj . sendNokStatus ( tbNames [ obj . ip ] [ unitId + section ] , date ) ;
if ( arguments [ "0" ] . err == "Offline" )
{
obj . socket . emit ( "close" ) ;
return ;
}
}
//! POSIELANIE NOK STATUSU - posle sa az pri poslednom registri z daneho unitu, nie pri kazdej chybnej hlaske
// if(obj.index + 1 == lenghtOfStreams)
// {
// obj.sendNokStatus(tbNames[obj.ip][unitId + section], date);
// }
// else if(obj.streams[obj.index + 1].unitId != unitId || obj.streams[obj.index + 1].section != section)
// {
// obj.sendNokStatus(tbNames[obj.ip][unitId + section], date);
// }
//console.error(require('util').inspect(arguments, {
// depth: null
//}))
obj . index ++ ;
setTimeout ( obj . readRegisters , 0 ) ;
} )
} ;
sendNokStatus = ( tbName , date ) => {
let dataToTB = {
[ tbName ] : [
{
"ts" : date ,
"values" : {
"status" : "NOK" ,
}
}
]
} ;
instance . send ( 1 , dataToTB ) ;
// console.log("poslane do tb po chybe", dataToTB[tbName][0], dataToTB);
} ;
sendData = ( response , register , date , unitId , section ) => {
let l = this . streams . length ;
for ( let i = 0 ; i < l ; i ++ ) {
let a = this . streams [ i ] ;
if ( a . name === register && a . unitId === unitId && a . section === section )
{
let tb _value = a . tb _value ;
let value ;
let l = response . length ;
let temp _val = 0 ;
if ( l === 2 )
{
temp _val = ( response [ 0 ] * ( 2 * * 16 ) + response [ 1 ] ) ;
if ( temp _val >= ( 2 * * 31 ) ) // ak je MSB bit nastavený, eventuálne sa dá použiť aj (temp_val & 0x80000000), ak vieš robiť logický súčin
{
//temp_val = temp_val - 2**31; // odstránim MSB bit, eventuálne sa dá použiť aj (temp_val & 0x7FFFFFFF), ak vieš robiť logický súčin
//temp_val = temp_val * (-1); // spravím z toho zápornú hodnotu
temp _val = temp _val - "0xFFFFFFFF" + 1 ;
}
}
else if ( l === 1 )
{
temp _val = response [ 0 ] ;
if ( temp _val >= ( 2 * * 15 ) ) // ak je MSB bit nastavený, eventuálne sa dá použiť aj (temp_val & 0x8000), ak vieš robiť logický súčin
{
// temp_val = temp_val - 2**15; // odstránim MSB bit, eventuálne sa dá použiť aj (temp_val & 0x7FFF), ak vieš robiť logický súčin
// temp_val = temp_val * (-1); // spravím z toho zápornú hodnotu
temp _val = temp _val - "0xFFFF" + 1 ;
}
}
value = temp _val ;
value = value / a . multiplier ;
let tbName = tbNames [ this . ip ] [ unitId . toString ( ) + section ] ;
// console.log(unitId, register, tbName, tb_value, response, a.multiplier, value, section);
// console.log(unitId, register, tb_value, value);
if ( tbName == undefined ) return ;
const values = {
"status" : "OK" ,
[ tb _value ] : value
} ;
2025-08-14 22:48:26 +02:00
// we send "energy_last_month" value, that is equal to "total_energy" value, on first day of new month ==> it means when month changes
2025-08-14 22:41:23 +02:00
if ( tb _value == "total_energy" )
{
const previousEnergy = a . previousEnergy ;
a . previousEnergy = value ;
if ( previousEnergy != null )
{
values [ "energy_update" ] = value - previousEnergy ;
}
2025-08-14 22:48:26 +02:00
2025-08-14 22:41:23 +02:00
const d = new Date ( date ) ;
const currentMonth = d . getMonth ( ) ;
const month = a . month ;
if ( month != currentMonth )
{
values [ "energy_last_month" ] = value ;
a . month = currentMonth ;
}
allModulesEnergy [ this . ip + '@' + unitId + section ] = value ;
2025-08-14 22:48:26 +02:00
if ( this . ip + '@' + unitId + section == "192.168.1.22@8A" )
{
allModulesEnergy [ this . ip + '@' + unitId + section + '_48' ] = value / 2 ;
allModulesEnergy [ this . ip + '@' + unitId + section + '_64' ] = value / 2 ;
}
2025-08-14 22:41:23 +02:00
}
let dataToTB = {
[ tbName ] : [
{
"ts" : date ,
"values" : values
}
]
} ;
instance . send ( 1 , dataToTB ) ;
2025-08-14 22:48:26 +02:00
// new code - ensures, we send values to another 4 devices, and "energy_last_month" value at the start of new month.
if ( this . distribute . hasOwnProperty ( tbName ) )
{
if ( valuesToRecount . includes ( tb _value ) )
{
value = value / 2 ;
const values = {
"status" : "OK" ,
[ tb _value ] : value
} ;
if ( tb _value == "total_energy" )
{
const previousEnergy = this . distribute [ tbName ] . previousEnergy ;
this . distribute [ tbName ] . previousEnergy = value ;
if ( previousEnergy != null )
{
values [ "energy_update" ] = value - previousEnergy ;
}
let d = new Date ( date ) ;
let month = d . getMonth ( ) ;
if ( month != this . distribute [ tbName ] . month )
{
values [ "energy_last_month" ] = value ;
this . distribute [ tbName ] . month = month ;
}
}
this . distribute [ tbName ] . tbNames . map ( sendTo => {
dataToTB = {
[ sendTo ] : [
{
"ts" : date ,
"values" : values
}
]
} ;
instance . send ( 1 , dataToTB ) ;
} )
}
}
2025-08-14 22:41:23 +02:00
break ;
}
}
} ;
}
2025-08-14 22:48:26 +02:00
const newSocket = new SocketWithClients ( "192.168.1.21" , conversionTable [ "192.168.1.21" ] ) ;
const newSocket2 = new SocketWithClients ( "192.168.1.22" , conversionTable [ "192.168.1.22" ] ) ;
2025-08-14 22:41:23 +02:00
}
2025-08-14 22:48:26 +02:00
//! VYPOCTY "energy_last_month_delta" pre virtualne devices za februar 2022 - zmazat neskor
// "192.168.1.22@8A":{"value":76772,"zone":"lithoman_48_64","tbname":"6lQGaY9RDywdVzObj0PZpb7Pg4NBn3exEK51LWZq"}
// "192.168.1.22@8A":{"value":50857,"zone":"lithoman_48_64","tbname":"6lQGaY9RDywdVzObj0PZpb7Pg4NBn3exEK51LWZq"}
// "energy_last_month_delta" = 25915
// --------
// { "nJL5lPMwBx23YpqRe0rq4bAdamXvWVbOrD4gNzy8": [ { "ts": 1643673891157, "values": { "energy_last_month_delta": 12958 } } ] }
// "nJL5lPMwBx23YpqRe0rq4bAdamXvWVbOrD4gNzy8", "ZmRwd93QL4gaezxEbAx1oK01prn2XjlPvGyqJ6BO"
// "192.168.1.22@8A_48":{"value":38386,"zone":"lithoman_48"},"192.168.1.22@8A_64":{"value":38386,"zone":"lithoman_64"}
// "192.168.1.22@8A_48":{"value":25428,"zone":"lithoman_48"},"192.168.1.22@8A_64":{"value":25428,"zone":"lithoman_64"}
// --------
// "192.168.1.21@11A":{"value":11830,"zone":"sitma_cmc","tbname":"PLBJzmK1r3Gynd6OW0g2yqAe5wV4vx9bDEqNgYR8"}
// "192.168.1.21@11A":{"value":7530,"zone":"sitma_cmc","tbname":"PLBJzmK1r3Gynd6OW0g2yqAe5wV4vx9bDEqNgYR8"}
// "energy_last_month_delta" = 4300
// -------
// 5915
// 3765
// = 2150
// this.distribute = {
// "6lQGaY9RDywdVzObj0PZpb7Pg4NBn3exEK51LWZq": {month: this.getCurrentMonth(), previousEnergy: null, tbNames: ["nJL5lPMwBx23YpqRe0rq4bAdamXvWVbOrD4gNzy8", "ZmRwd93QL4gaezxEbAx1oK01prn2XjlPvGyqJ6BO"]},
// "PLBJzmK1r3Gynd6OW0g2yqAe5wV4vx9bDEqNgYR8": {month: this.getCurrentMonth(), previousEnergy: null, tbNames: ["E6Kg9oDnLWyzPRMva7vWR9AJxp4VG58qO2w1lZYe","roKgWqY95V3mXMRzyAjrYnAbLjexpJPvaGDBw826"]}
// }
// { "E6Kg9oDnLWyzPRMva7vWR9AJxp4VG58qO2w1lZYe": [ { "ts": 1643673891157, "values": { "energy_last_month_delta": 2150 } } ] }