Skip to content

Commit

Permalink
refactor SMA EnergyMeter / HomeManager module: Use ValueStore and cle…
Browse files Browse the repository at this point in the history
…an-up code
  • Loading branch information
yankee42 committed Jan 14, 2022
1 parent e10683b commit d7f92c8
Show file tree
Hide file tree
Showing 3 changed files with 40 additions and 117 deletions.
7 changes: 3 additions & 4 deletions modules/bezug_smashm/main.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
#!/bin/bash

timeout 3 python3 /var/www/html/openWB/modules/bezug_smashm/sma_em_measurement.py $smashmbezugid
wattbezug=$(</var/www/html/openWB/ramdisk/wattbezug)
echo $wattbezug
OPENWBBASEDIR=$(cd `dirname $0`/../../ && pwd)
timeout 3 python3 "$OPENWBBASEDIR/modules/bezug_smashm/sma_em_measurement.py" "$smashmbezugid" >> "$OPENWBBASEDIR/ramdisk/openWB.log" 2>&1
cat /var/www/html/openWB/ramdisk/wattbezug
148 changes: 35 additions & 113 deletions modules/bezug_smashm/sma_em_measurement.py
Original file line number Diff line number Diff line change
@@ -1,80 +1,26 @@
#!/usr/bin/env python3
# coding=utf-8
"""
*
* by Wenger Florian 2015-09-02
* [email protected]
*
* endless loop (until ctrl+c) displays measurement from SMA Energymeter
*
*
* this software is released under GNU General Public License, version 2.
* This program 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; version 2 of the License.
* This program 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 this program;
* if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* 2018-12-22 Tommi2Day small enhancements
* 2019-08-13 datenschuft run without config
* 2020-01-04 datenschuft changes to tun with speedwiredecoder
* 2020-01-13 Kevin Wieland changes to run with openWB
* 2020-02-03 theHolgi added phase-wise load and power factor
*
*/
"""

import signal
import logging
import socket
import struct
import sys
from pathlib import Path
from math import copysign
from typing import Optional

from helpermodules.cli import run_using_positional_cli_args
from helpermodules.log import setup_logging_stdout
from modules.common.component_state import CounterState
from modules.common.store import get_counter_value_store
from speedwiredecoder import decode_speedwire


# clean exit
def abortprogram(signal,frame):
# Housekeeping -> nothing to cleanup
print('STRG + C = end program')
sys.exit(0)

def writeToFile(filename, content):
Path(filename).write_text(str(content))

# abort-signal
signal.signal(signal.SIGINT, abortprogram)


#read configuration
#parser = ConfigParser()
#default values
ipbind = '0.0.0.0'
MCAST_GRP = '239.12.255.254'
MCAST_PORT = 9522

basepath = str(Path(__file__).parents[2] / "ramdisk") + "/"
# filename: channel
mappingdict = { 'evuhz': 'frequency' }
phasemappingdict = { 'bezuga%i': { 'from': 'i%i', 'sign': True },
'evuv%i': { 'from': 'u%i' },
'evupf%i': { 'from': 'cosphi%i' }
}
#try:
# smaemserials=parser.get('SMA-EM', 'serials')
# ipbind=parser.get('DAEMON', 'ipbind')
# MCAST_GRP = parser.get('DAEMON', 'mcastgrp')
# MCAST_PORT = int(parser.get('DAEMON', 'mcastport'))
#except:
# print('Cannot find config /etc/smaemd/config... using defaults')
log = logging.getLogger("SMA HomeManager")


def run(smaserials: Optional[str] = None):
def run(sma_serials: Optional[str] = None):
log.debug("Beginning update")
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(('', MCAST_PORT))
Expand All @@ -85,62 +31,38 @@ def run(smaserials: Optional[str] = None):
print('could not connect to mulicast group or bind to given interface')
sys.exit(1)
# processing received messages
while not process_datagram(sock.recv(608), smaserials):
while not process_datagram(sock.recv(608), sma_serials):
pass
log.debug("Update completed successfully")


def process_datagram(datagram: bytes, smaserials: Optional[str] = None):
#Paket ignorieren, wenn es nicht dem SMA-"energy meter protocol" mit protocol id = 0x6069 entspricht
def process_datagram(datagram: bytes, sma_serials: Optional[str] = None):
# Paket ignorieren, wenn es nicht dem SMA-"energy meter protocol" mit protocol id = 0x6069 entspricht
if datagram[16:18] != b'\x60\x69':
return
emparts=decode_speedwire(datagram)
# Output...
# don't know what P,Q and S means:
# http://en.wikipedia.org/wiki/AC_power or http://de.wikipedia.org/wiki/Scheinleistung
# thd = Total_Harmonic_Distortion http://de.wikipedia.org/wiki/Total_Harmonic_Distortion
# cos phi is always positive, no matter what quadrant
positive = [ 1,1,1,1 ]
if smaserials is None or smaserials == 'none' or str(emparts['serial']) == smaserials:
# Special treatment for positive / negative power

try:
watt=int(emparts['pconsume'])
if watt < 5:
watt=-int(emparts['psupply'])
positive[0] = -1
writeToFile(basepath + 'wattbezug', watt)
except:
pass
try:
if ( emparts['psupplycounter'] < 900000 ):
writeToFile(basepath + 'einspeisungkwh', emparts['psupplycounter'] * 1000)
except:
pass
try:
writeToFile(basepath + 'bezugkwh', emparts['pconsumecounter'] * 1000)
except:
pass
try:
for phase in [1,2,3]:
power = int(emparts['p%iconsume' % phase])
if power < 5:
power = -int(emparts['p%isupply' % phase])
positive[phase] = -1
writeToFile(basepath + 'bezugw%i' % phase, power)
for filename, mapping in phasemappingdict.items():
for phase in [1,2,3]:
if mapping['from'] % phase in emparts:
value = emparts[mapping['from'] % phase]
if 'sign' in mapping and mapping['sign']:
value *= positive[phase]
writeToFile(basepath + filename % phase, value)
for filename, key in mappingdict.items():
if key in emparts:
writeToFile(basepath + filename, emparts[key])
except:
pass
sma_data = decode_speedwire(datagram)
if sma_serials is None or sma_serials == 'none' or str(sma_data['serial']) == sma_serials:
def get_power(phase_str: str = ""):
# "consume" and "supply" are always >= 0. Thus we need to check both "supply" and "consume":
power_import = sma_data["p" + phase_str + "consume"]
return -sma_data["p" + phase_str + "supply"] if power_import == 0 else power_import

powers = [get_power(str(phase)) for phase in range(1, 4)]

get_counter_value_store(1).set(CounterState(
imported=sma_data['pconsumecounter'] * 1000,
exported=sma_data['psupplycounter'] * 1000,
power=get_power(),
voltages=[sma_data["u" + str(phase)] for phase in range(1, 4)],
# currents reported are always absolute values. We get the sign from power:
currents=[copysign(sma_data["i" + str(phase)], powers[phase - 1]) for phase in range(1, 4)],
powers=powers,
power_factors=[sma_data["cosphi" + str(phase)] for phase in range(1, 4)],
frequency=sma_data.get("frequency")
))
return True


if __name__ == '__main__':
run(sys.argv[1] if len(sys.argv) > 1 else None)
setup_logging_stdout()
run_using_positional_cli_args(run)
2 changes: 2 additions & 0 deletions modules/bezug_smashm/sma_em_measurement_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import pytest

import sma_em_measurement as sma
from helpermodules import compatibility
from test_utils.mock_ramdisk import MockRamdisk

# This sample was collected from an SMA Energy Meter with Firmware 2.0.18.R on 2021-12-22:
Expand All @@ -23,6 +24,7 @@

@pytest.fixture
def mock_ramdisk(monkeypatch):
monkeypatch.setattr(compatibility, "is_ramdisk_in_use", lambda : True)
return MockRamdisk(monkeypatch)


Expand Down

0 comments on commit d7f92c8

Please sign in to comment.