Skip to content

Commit

Permalink
Merge pull request #896 from CannonRS/ksemmodbus
Browse files Browse the repository at this point in the history
Ksemmodbus
  • Loading branch information
Morg42 authored Feb 25, 2024
2 parents b777ec7 + 800d3e3 commit 608a284
Show file tree
Hide file tree
Showing 11 changed files with 419 additions and 55 deletions.
13 changes: 5 additions & 8 deletions ksemmodbus/README.md
Original file line number Diff line number Diff line change
@@ -1,24 +1,21 @@
# Modbus Plugin for Kostal Smart Energy Meter

#### Version 1.6.2
#### Version 1.6.3

This plugin connects your Kostal Smart Energy Meter (https://www.kostal-solar-electric.com/) via ModBus with SmarthomeNG.
- read out all Smart Meter data

## Change history

- work with newer versions of pymodbus too

### Changes Since version 1.x.x
## Change history

- No Changes so far
- support for pymodbus 2 dropped


### Requirements needed software

* Python > 3.5
* Python >= 3.8
* pip install pymodbus
* SmarthomeNG >= 1.6.0
* SmarthomeNG >= 1.8.0

## Configuration

Expand Down
2 changes: 1 addition & 1 deletion ksemmodbus/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
from .ksem import Ksem

class Ksemmodbus(SmartPlugin):
PLUGIN_VERSION = '1.6.2'
PLUGIN_VERSION = '1.6.3'
ksem = 'None'
_items = []

Expand Down
71 changes: 71 additions & 0 deletions ksemmodbus/_pv_1_6_2/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Modbus Plugin for Kostal Smart Energy Meter

#### Version 1.6.2

This plugin connects your Kostal Smart Energy Meter (https://www.kostal-solar-electric.com/) via ModBus with SmarthomeNG.
- read out all Smart Meter data

## Change history

- work with newer versions of pymodbus too

### Changes Since version 1.x.x

- No Changes so far


### Requirements needed software

* Python > 3.5
* pip install pymodbus
* SmarthomeNG >= 1.6.0

## Configuration

### 1) /smarthome/etc/plugin.yaml

Enable the plugin in plugin.yaml, type in the Smart Meters IP address and configure the ModBus Port and update cycle(seconds).

```yaml
Ksemmodbus:
plugin_name: ksemmodbus
ksem_ip: 'XXX.XXX.XXX.XXX'
modbus_port: '502'
update_cycle: '20'
```
### 2) /smarthome/items/kostal.yaml
Create an item based on the template files/kostal_item_template.yaml
## Examples
Thats it! Now you can start using the plugin within SmartVisu.
For example:
#### Get data from Energy Meter:
```html
<p>Active Power - : {{basic.value('KSEM_Beszug','Kostal.ksem.ksem_0','W')}} </p>
<p>Active Power + : {{basic.value('KSEM_Einspeisen','Kostal.ksem.ksem_2','W')}} </p>

```
#### The following data are stored in the respective items:
| Addr (dec) | Description | Format | Unit |
|-------------------|---------------------------------------------------|--------|---------|
| ksem_0 | Active power+ | U32 | W |
| ksem_2 | Active power- | U32 | W |
| ksem_4 | Reactive power+ | U32 | var |
| ksem_6 | Reactive power- | U32 | var |
| ksem_16 | Apparent power+ | U32 | VA |
| ksem_18 | Apparent power- | U32 | VA |
| ksem_24 | Power factor | Float | - |
| ksem_512 | Active energy+ | U64 | Wh |
| ksem_516 | Active energy- | U64 | Wh |
| ksem_520 | Reactive energy+ | U64 | varh |
| ksem_524 | Reactive energy- | U64 | varh |
| ksem_544 | Apparent energy+ | U64 | VAh |
| ksem_548 | Apparent energy- | U64 | VAh |
62 changes: 62 additions & 0 deletions ksemmodbus/_pv_1_6_2/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
#!/usr/bin/env python3
# vim: set encoding=utf-8 tabstop=4 softtabstop=4 shiftwidth=4 expandtab
#########################################################################
# Copyright 2019 Thomas Hengsberg <[email protected]>
# Copyright 2022 Ronny Schulz
#########################################################################
# This file is part of SmartHomeNG.
#
# Sample plugin for new plugins to run with SmartHomeNG version 1.4 and
# upwards.
#
# SmartHomeNG is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# SmartHomeNG is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with SmartHomeNG. If not, see <http://www.gnu.org/licenses/>.
#
#########################################################################

from lib.model.smartplugin import *

from .ksem import Ksem

class Ksemmodbus(SmartPlugin):
PLUGIN_VERSION = '1.6.2'
ksem = 'None'
_items = []

def __init__(self, sh, *args, **kwargs):
self.ksem = Ksem(self.get_parameter_value("ksem_ip"),self.get_parameter_value("modbus_port"))
self._cycle = int(self.get_parameter_value("update_cycle"))
return

def run(self):
self.logger.debug("Run method called")
self.scheduler_add('poll_device', self.poll_device, cycle=self._cycle)
self.alive = True

def stop(self):
self.logger.debug("Stop method called")
self.alive = False

def parse_item(self, item):
for i in self.ksem.decRow:
s = 'ksem_' + str(i)
if self.has_iattr(item.conf, s):
self._items.append(item)

def poll_device(self):
ksem_data = self.ksem.get_data()
for item in self._items:
for i in range (0,len(ksem_data)):
s = 'ksem_' + str(ksem_data[i].adrDec)
if self.has_iattr(item.conf, s):
item(self.ksem.registers[i].value)
58 changes: 58 additions & 0 deletions ksemmodbus/_pv_1_6_2/files/kostal_item_template.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
Kostal:
ksem:
ksem_0:
ksem_0: ''
visu_acl: ro
type: num
ksem_2:
ksem_2: ''
visu_acl: ro
type: num
ksem_0:
ksem_0: ''
visu_acl: ro
type: num
ksem_4:
ksem_4: ''
visu_acl: ro
type: num
ksem_6:
ksem_6: ''
visu_acl: ro
type: num
ksem_16:
ksem_16: ''
visu_acl: ro
type: num
ksem_18:
ksem_18: ''
visu_acl: ro
type: num
ksem_24:
ksem_24: ''
visu_acl: ro
type: num
ksem_512:
ksem_512: ''
visu_acl: ro
type: num
ksem_516:
ksem_516: ''
visu_acl: ro
type: num
ksem_5520:
ksem_520: ''
visu_acl: ro
type: num
ksem_524:
ksem_524: ''
visu_acl: ro
type: num
ksem_544:
ksem_544: ''
visu_acl: ro
type: num
ksem_548:
ksem_548: ''
visu_acl: ro
type: num
122 changes: 122 additions & 0 deletions ksemmodbus/_pv_1_6_2/ksem.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import binascii
import hashlib
import hmac
import locale
import time
import logging
import pymodbus

from lib.model.smartplugin import *

# pymodbus library from https://github.com/riptideio/pymodbus
from pymodbus.version import version
pymodbus_baseversion = int(version.short().split('.')[0])

if pymodbus_baseversion > 2:
# for newer versions of pymodbus
from pymodbus.client.tcp import ModbusTcpClient
else:
# for older versions of pymodbus
from pymodbus.client.sync import ModbusTcpClient

from pymodbus.payload import BinaryPayloadDecoder
from pymodbus.constants import Endian

class Ksem:
def __init__(self, ip, port):
self.__init_registers()
self.client = ModbusTcpClient(ip, port=port)

def __init_registers(self):
self.registers = []
for i in range(0, len(self.decRow)):
reg = Register(self.decRow[i],self.__descriptionRow[i],self.__typeRow[i],self.__unitRow[i])
self.registers.append(reg)

def get_data(self):
try:
self.client.connect()
for i in self.registers:
if i.type == "Float":
i.value = self.__read_float(i.adrDec)
elif i.type == "U16":
i.value = self.__read_u16(i.adrDec)
elif i.type == "U16_2":
i.value = self.__read_u16_2(i.adrDec)
elif i.type == "U32":
i.value = self.__read_u32(i.adrDec)
elif i.type == "U64":
i.value = self.__read_u64(i.adrDec)
elif i.type == "S16":
i.value = self.__read_s16(i.adrDec)
except Exception as exc:
print("Error getting Data from Kostal Smart Energy Meter :", exc)

self.client.close()
return self.registers

def __read_float(self, adr_dec):
if pymodbus_baseversion > 2:
result = self.client.read_holding_registers(adr_dec, 2, slave=71)
else:
result = self.client.read_holding_registers(adr_dec, 2, unit=71)
float_value = BinaryPayloadDecoder.fromRegisters(result.registers,byteorder=Endian.Big,wordorder=Endian.Little)
return round(float_value.decode_32bit_float(), 2)

def __read_u16(self, adr_dec):
if pymodbus_baseversion > 2:
result = self.client.read_holding_registers(adr_dec, 1, slave=71)
else:
result = self.client.read_holding_registers(adr_dec, 1, unit=71)
u16_value = BinaryPayloadDecoder.fromRegisters(result.registers, byteorder=Endian.Big,wordorder=Endian.Little)
return u16_value.decode_16bit_uint()

def __read_u16_2(self, adr_dec):
if pymodbus_baseversion > 2:
result = self.client.read_holding_registers(adr_dec, 2, slave=71)
else:
result = self.client.read_holding_registers(adr_dec, 2, unit=71)
u16_2_value = BinaryPayloadDecoder.fromRegisters(result.registers, byteorder=Endian.Big,wordorder=Endian.Little)
return u16_2_value.decode_16bit_uint()

def __read_s16(self, adr_dec):
if pymodbus_baseversion > 2:
result = self.client.read_holding_registers(adr_dec, 1, slave=71)
else:
result = self.client.read_holding_registers(adr_dec, 1, unit=71)
s16_value = BinaryPayloadDecoder.fromRegisters(result.registers, byteorder=Endian.Big,wordorder=Endian.Little)
return s16_value.decode_16bit_uint()

def __read_u32(self, adr_dec):
if pymodbus_baseversion > 2:
result = self.client.read_holding_registers(adr_dec, 2, slave=71)
else:
result = self.client.read_holding_registers(adr_dec, 2, unit=71)
u32_value = BinaryPayloadDecoder.fromRegisters(result.registers, byteorder=Endian.Big, wordorder=Endian.Big)
return round((u32_value.decode_32bit_uint() * 0.1), 2)

def __read_u64(self, adr_dec):
if pymodbus_baseversion > 2:
result = self.client.read_holding_registers(adr_dec, 4, slave=71)
else:
result = self.client.read_holding_registers(adr_dec, 4, unit=71)
u64_value = BinaryPayloadDecoder.fromRegisters(result.registers, byteorder=Endian.Big, wordorder=Endian.Big)
return round((u64_value.decode_64bit_uint() * 0.0001),2)


decRow = [0, 2, 4, 6, 16, 18, 24, 512, 516, 520, 524, 544, 548]

__descriptionRow = ['Active Power +', 'Active Power -', 'Reactive Power +', 'Reactive Power -', 'Apparent power +', 'Apparent power -', 'Power Factor', 'Active energy+', 'Active energy-', 'Reactive energy+', 'Reactive energy-', 'Apparent energy+', 'Apparent energy-']

__unitRow = ['W', 'W', 'var', 'var', 'VA', 'VA', '', 'Wh', 'Wh' ,'varh', 'varh', 'VAh', 'VAh']

__typeRow = ['U32', 'U32', 'U32', 'U32', 'U32', 'U32', 'Float', 'U64', 'U64', 'U64', 'U64', 'U64', 'U64']


class Register:
def __init__(self, adr_dec,description,val_type,unit):
self.adrDec = adr_dec
self.description = description
self.type = val_type
self.unit = unit
self.value = ''
Loading

0 comments on commit 608a284

Please sign in to comment.