11 Commits
0.0.9 ... 0.1.4

Author SHA1 Message Date
532b5b7211 read coils 6
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful
2025-12-15 11:45:55 +01:00
25f6a1f43f read coils 5
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful
2025-12-15 11:35:52 +01:00
a5f9527f4d read coils 4
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful
2025-12-15 11:20:46 +01:00
08c1faf606 read coils 3
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful
2025-12-15 11:09:21 +01:00
00afad4a3d read coils 2 2025-12-15 11:07:41 +01:00
086c240638 read coils 2025-12-15 11:06:36 +01:00
93bbccf5c3 fix typo in configuration
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful
2024-01-11 15:32:32 +01:00
012bb46b2a fix configuration
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful
2024-01-11 15:27:28 +01:00
ae1828a06e fix use of modbus module
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful
2024-01-11 15:23:07 +01:00
51dec2b281 fix ci script
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful
2024-01-11 15:16:47 +01:00
4ea85f3c7e change config to env, add ci-script
All checks were successful
ci/woodpecker/tag/woodpecker Pipeline was successful
2024-01-11 14:52:36 +01:00
8 changed files with 138 additions and 28 deletions

30
.woodpecker.yml Normal file
View File

@@ -0,0 +1,30 @@
steps:
build:
image: plugins/kaniko
settings:
repo: gitea.hottis.de/wn/digitaltwin1
registry:
from_secret: local_registry
tags: latest,${CI_COMMIT_TAG}
username:
from_secret: local_username
password:
from_secret: local_password
dockerfile: Dockerfile
when:
- event: tag
deploy:
image: portainer/kubectl-shell:latest
environment:
KUBE_CONFIG_CONTENT:
from_secret: kube_config
commands:
- export IMAGE_TAG=$CI_COMMIT_TAG
- printf "$KUBE_CONFIG_CONTENT" > /tmp/kubeconfig
- export KUBECONFIG=/tmp/kubeconfig
- cat ./deployment/install-yml.tmpl | sed -e 's,%IMAGETAG%,'$IMAGE_TAG','g | kubectl apply -f -
when:
- event: tag

View File

@@ -11,7 +11,7 @@ ARG CONF_DIR="${APP_DIR}/config"
RUN \ RUN \
apt update && \ apt update && \
pip3 install loguru && \ pip3 install loguru && \
pip3 install pymodbus && \ pip3 install pymodbus==3.6.3 && \
pip3 install paho-mqtt pip3 install paho-mqtt
RUN \ RUN \

View File

@@ -0,0 +1,45 @@
apiVersion: v1
kind: Namespace
metadata:
name: homea
---
apiVersion: v1
kind: ConfigMap
metadata:
name: digitaltwin1
namespace: homea
data:
MQTT__BROKER: "emqx01-anonymous-cluster-internal.broker.svc.cluster.local"
MQTT__DIGITALOUTPUTTOPICPREFIX: "dt1/coil"
MQTT__DIGITALINPUTTOPICPREFIX: "dt1/di"
MQTT__COILINPUTTOPICPREFIX: "dt1/ci"
MQTT__ANALOGINPUTEVENTTOPICPREFIX: "dt1/ai/event"
MQTT__ANALOGINPUTPERIODICTOPICPREFIX: "dt1/ai/periodic"
MQTT__ANALOGINPUTPUBLISHPERIOD: "60.0"
MQTT__DISABLEANALOGINPUTEVENTPUBLISHING: "true"
MODBUS__CLIENT: "172.16.2.31"
MODBUS__SCANRATE: "0.25"
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: digitaltwin1
namespace: homea
labels:
app: digitaltwin1
spec:
replicas: 1
selector:
matchLabels:
app: digitaltwin1
template:
metadata:
labels:
app: digitaltwin1
spec:
containers:
- name: digitaltwin1
image: gitea.hottis.de/wn/digitaltwin1:%IMAGETAG%
envFrom:
- configMapRef:
name: digitaltwin1

View File

@@ -1,4 +1,4 @@
from pymodbus.client.sync import ModbusTcpClient as ModbusClient from pymodbus.client import ModbusTcpClient as ModbusClient
from pymodbus.exceptions import ModbusIOException from pymodbus.exceptions import ModbusIOException
from time import sleep from time import sleep
import threading import threading
@@ -39,17 +39,17 @@ class ModbusHandler(threading.Thread):
self.processImage.init(digitalOutputBits, digitalInputBits, analogInputBits) self.processImage.init(digitalOutputBits, digitalInputBits, analogInputBits)
reg = client.read_coils(0, digitalOutputBits)
if isinstance(reg, ModbusIOException):
raise Exception(reg)
with self.processImage:
self.processImage.setCoils(reg.bits)
while not self.killBill: while not self.killBill:
try: try:
if not client.is_socket_open(): if not client.is_socket_open():
client.connect() client.connect()
reg = client.read_coils(0, digitalOutputBits)
if isinstance(reg, ModbusIOException):
raise Exception(reg)
readCoils = reg.bits
reg = client.read_input_registers(0, analogInputBits // 8) reg = client.read_input_registers(0, analogInputBits // 8)
if isinstance(reg, ModbusIOException): if isinstance(reg, ModbusIOException):
raise Exception(reg) raise Exception(reg)
@@ -64,6 +64,7 @@ class ModbusHandler(threading.Thread):
with self.processImage: with self.processImage:
self.processImage.setAnalogsInputs(analogInputs) self.processImage.setAnalogsInputs(analogInputs)
self.processImage.setDiscreteInputs(discreteInputs) self.processImage.setDiscreteInputs(discreteInputs)
self.processImage.setCoils(readCoils)
if self.processImage.hasPendingInputChanges(): if self.processImage.hasPendingInputChanges():
self.processImage.notify() self.processImage.notify()
if self.processImage.hasPendingOutputChanges(): if self.processImage.hasPendingOutputChanges():

View File

@@ -22,6 +22,7 @@ class MqttEventPublisher(AbstractMqttPublisher):
continue continue
discreteInputChangeset = self.processImage.getChangedDiscreteInputs() discreteInputChangeset = self.processImage.getChangedDiscreteInputs()
coilInputChangeset = self.processImage.getChangedCoils()
if not self.disableAnalogInputEventPublishing: if not self.disableAnalogInputEventPublishing:
analogInputChangeset = self.processImage.getChangedAnalogsInputs() analogInputChangeset = self.processImage.getChangedAnalogsInputs()
@@ -34,6 +35,15 @@ class MqttEventPublisher(AbstractMqttPublisher):
str(discreteInputChangeItem[1][0]), str(discreteInputChangeItem[1][0]),
retain=True) retain=True)
for coilInputChangeItem in coilInputChangeset:
logger.debug("Coil input {} changed from {} to {}"
.format(coilInputChangeItem[0],
coilInputChangeItem[1][1],
coilInputChangeItem[1][0]))
self.client.publish("{}/{}".format(self.config["coilInputTopicPrefix"], str(coilInputChangeItem[0])),
str(coilInputChangeItem[1][0]),
retain=True)
if not self.disableAnalogInputEventPublishing: if not self.disableAnalogInputEventPublishing:
for analogInputChangeItem in analogInputChangeset: for analogInputChangeItem in analogInputChangeset:
logger.debug("Analog input {} changed from {} to {}" logger.debug("Analog input {} changed from {} to {}"

View File

@@ -16,6 +16,8 @@ class ProcessImage(Condition):
self.numCoils = numCoils self.numCoils = numCoils
self.coils = [] self.coils = []
self.shadowCoils = [ None ] * numCoils self.shadowCoils = [ None ] * numCoils
self.readCoils = [ None ] * numCoils
self.readShadowCoils = [ None ] * numCoils
self.numDiscreteInputs = numDiscreteInputs self.numDiscreteInputs = numDiscreteInputs
self.discreteInputs = [] self.discreteInputs = []
@@ -31,7 +33,7 @@ class ProcessImage(Condition):
return self.initialized return self.initialized
def hasPendingInputChanges(self): def hasPendingInputChanges(self):
return (self.discreteInputs != self.shadowDiscreteInputs) or (self.analogInputs != self.shadowAnalogInputs) return (self.discreteInputs != self.shadowDiscreteInputs) or (self.analogInputs != self.shadowAnalogInputs) or (self.readCoils != self.readShadowCoils)
def hasPendingOutputChanges(self): def hasPendingOutputChanges(self):
return self.shadowCoils != self.coils return self.shadowCoils != self.coils
@@ -56,6 +58,8 @@ class ProcessImage(Condition):
def setCoils(self, coils): def setCoils(self, coils):
if not self.initialized: if not self.initialized:
raise NotInitializedException raise NotInitializedException
self.readCoils = coils
if self.coils == []:
self.coils = coils self.coils = coils
def setCoil(self, coilNum, value): def setCoil(self, coilNum, value):
@@ -64,12 +68,12 @@ class ProcessImage(Condition):
self.coils[coilNum] = value self.coils[coilNum] = value
self.coilEvent.set() self.coilEvent.set()
# def getChangedCoils(self): def getChangedCoils(self):
# if not self.initialized: if not self.initialized:
# raise NotInitializedException raise NotInitializedException
# changedCoils = zippingFilter(self.coils, self.shadowCoils) changedCoils = zippingFilter(self.coils, self.readCoils)
# self.shadowCoils = self.coils self.readShadowCoils = self.coils
# return changedCoils return changedCoils
def getCoils(self): def getCoils(self):
if not self.initialized: if not self.initialized:

31
src/config.py Normal file
View File

@@ -0,0 +1,31 @@
import os
from loguru import logger
class Config:
OPTIONS = {
'mqtt': [ 'broker',
'digitalOutputTopicPrefix',
'digitalInputTopicPrefix',
'coilInputTopicPrefix',
'analogInputEventTopicPrefix',
'analogInputPeriodicTopicPrefix',
'analogInputPublishPeriod',
'disableAnalogInputEventPublishing' ],
'modbus': [ 'client', 'scanrate' ]
}
def __init__(self):
self.values = {}
for section, keys in Config.OPTIONS.items():
self.values[section] = {}
for key in keys:
varname = f"{section}__{key}".upper()
try:
self.values[section][key] = os.environ[varname]
logger.info(f"Config: {section} {key} -> {self.values[section][key]}")
except KeyError:
pass
def __getitem__(self, section):
return self.values[section]

View File

@@ -8,7 +8,7 @@ from ModbusHandler import ModbusHandler
from MqttEventPublisher import MqttEventPublisher, mqttEventPublisherStart from MqttEventPublisher import MqttEventPublisher, mqttEventPublisherStart
from MqttPeriodPublisher import MqttPeriodPublisher from MqttPeriodPublisher import MqttPeriodPublisher
from MqttCoilSubscriber import MqttCoilSubscriber from MqttCoilSubscriber import MqttCoilSubscriber
from config import Config
deathBell = threading.Event() deathBell = threading.Event()
@@ -21,18 +21,7 @@ def exceptHook(args):
logger.info("DigitalTwin1 starting") logger.info("DigitalTwin1 starting")
parser = argparse.ArgumentParser(description="DigitalTwin1") config = Config()
parser.add_argument('--config', '-f',
help='Config file, default is $pwd/config/config.ini',
required=False,
default='./config/config.ini')
args = parser.parse_args()
config = configparser.ConfigParser()
config.read(args.config)
processImage = ProcessImage() processImage = ProcessImage()