Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
532b5b7211
|
|||
|
25f6a1f43f
|
|||
|
a5f9527f4d
|
|||
|
08c1faf606
|
|||
|
00afad4a3d
|
|||
|
086c240638
|
|||
|
93bbccf5c3
|
|||
|
012bb46b2a
|
|||
|
ae1828a06e
|
|||
|
51dec2b281
|
|||
|
4ea85f3c7e
|
30
.woodpecker.yml
Normal file
30
.woodpecker.yml
Normal 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
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ ARG CONF_DIR="${APP_DIR}/config"
|
||||
RUN \
|
||||
apt update && \
|
||||
pip3 install loguru && \
|
||||
pip3 install pymodbus && \
|
||||
pip3 install pymodbus==3.6.3 && \
|
||||
pip3 install paho-mqtt
|
||||
|
||||
RUN \
|
||||
|
||||
45
deployment/install-yml.tmpl
Normal file
45
deployment/install-yml.tmpl
Normal 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
|
||||
@@ -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 time import sleep
|
||||
import threading
|
||||
@@ -39,17 +39,17 @@ class ModbusHandler(threading.Thread):
|
||||
|
||||
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:
|
||||
try:
|
||||
if not client.is_socket_open():
|
||||
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)
|
||||
if isinstance(reg, ModbusIOException):
|
||||
raise Exception(reg)
|
||||
@@ -64,6 +64,7 @@ class ModbusHandler(threading.Thread):
|
||||
with self.processImage:
|
||||
self.processImage.setAnalogsInputs(analogInputs)
|
||||
self.processImage.setDiscreteInputs(discreteInputs)
|
||||
self.processImage.setCoils(readCoils)
|
||||
if self.processImage.hasPendingInputChanges():
|
||||
self.processImage.notify()
|
||||
if self.processImage.hasPendingOutputChanges():
|
||||
|
||||
@@ -22,6 +22,7 @@ class MqttEventPublisher(AbstractMqttPublisher):
|
||||
continue
|
||||
|
||||
discreteInputChangeset = self.processImage.getChangedDiscreteInputs()
|
||||
coilInputChangeset = self.processImage.getChangedCoils()
|
||||
if not self.disableAnalogInputEventPublishing:
|
||||
analogInputChangeset = self.processImage.getChangedAnalogsInputs()
|
||||
|
||||
@@ -34,6 +35,15 @@ class MqttEventPublisher(AbstractMqttPublisher):
|
||||
str(discreteInputChangeItem[1][0]),
|
||||
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:
|
||||
for analogInputChangeItem in analogInputChangeset:
|
||||
logger.debug("Analog input {} changed from {} to {}"
|
||||
|
||||
@@ -16,6 +16,8 @@ class ProcessImage(Condition):
|
||||
self.numCoils = numCoils
|
||||
self.coils = []
|
||||
self.shadowCoils = [ None ] * numCoils
|
||||
self.readCoils = [ None ] * numCoils
|
||||
self.readShadowCoils = [ None ] * numCoils
|
||||
|
||||
self.numDiscreteInputs = numDiscreteInputs
|
||||
self.discreteInputs = []
|
||||
@@ -31,7 +33,7 @@ class ProcessImage(Condition):
|
||||
return self.initialized
|
||||
|
||||
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):
|
||||
return self.shadowCoils != self.coils
|
||||
@@ -56,7 +58,9 @@ class ProcessImage(Condition):
|
||||
def setCoils(self, coils):
|
||||
if not self.initialized:
|
||||
raise NotInitializedException
|
||||
self.coils = coils
|
||||
self.readCoils = coils
|
||||
if self.coils == []:
|
||||
self.coils = coils
|
||||
|
||||
def setCoil(self, coilNum, value):
|
||||
if not self.initialized:
|
||||
@@ -64,12 +68,12 @@ class ProcessImage(Condition):
|
||||
self.coils[coilNum] = value
|
||||
self.coilEvent.set()
|
||||
|
||||
# def getChangedCoils(self):
|
||||
# if not self.initialized:
|
||||
# raise NotInitializedException
|
||||
# changedCoils = zippingFilter(self.coils, self.shadowCoils)
|
||||
# self.shadowCoils = self.coils
|
||||
# return changedCoils
|
||||
def getChangedCoils(self):
|
||||
if not self.initialized:
|
||||
raise NotInitializedException
|
||||
changedCoils = zippingFilter(self.coils, self.readCoils)
|
||||
self.readShadowCoils = self.coils
|
||||
return changedCoils
|
||||
|
||||
def getCoils(self):
|
||||
if not self.initialized:
|
||||
|
||||
31
src/config.py
Normal file
31
src/config.py
Normal 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]
|
||||
|
||||
@@ -8,7 +8,7 @@ from ModbusHandler import ModbusHandler
|
||||
from MqttEventPublisher import MqttEventPublisher, mqttEventPublisherStart
|
||||
from MqttPeriodPublisher import MqttPeriodPublisher
|
||||
from MqttCoilSubscriber import MqttCoilSubscriber
|
||||
|
||||
from config import Config
|
||||
|
||||
deathBell = threading.Event()
|
||||
|
||||
@@ -21,18 +21,7 @@ def exceptHook(args):
|
||||
|
||||
logger.info("DigitalTwin1 starting")
|
||||
|
||||
parser = argparse.ArgumentParser(description="DigitalTwin1")
|
||||
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)
|
||||
|
||||
|
||||
|
||||
config = Config()
|
||||
|
||||
processImage = ProcessImage()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user