Skip to content
Snippets Groups Projects
Commit efa8d9dd authored by Adrien Lescourt's avatar Adrien Lescourt
Browse files

initial commit

parents
No related branches found
No related tags found
No related merge requests found
saved_rooms.json
actuasim.log
.idea/
# Created by .ignore support plugin (hsz.mobi)
### Python template
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*,cover
.hypothesis/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# IPython Notebook
.ipynb_checkpoints
# pyenv
.python-version
# celery beat schedule file
celerybeat-schedule
# dotenv
.env
# virtualenv
venv/
ENV/
# Spyder project settings
.spyderproject
# Rope project settings
.ropeproject
# ACTUASIM
The aim of Actuasim is to reproduce a very simple building KNX automation.
It can be used to reproduce an existing building, in order to develop the KNX automation without disturbing people in their offices.
It replaces a KNXnet/IP gateway, handles all received datagram and triggers the corresponding actions. And as much as the real building, blinds and valves positions can be set by the user as well.
![Actuasim](https://github.com/leadrien/actuasim/blob/master/actuasim.png)
# USAGE
The program is written in Python3, and depends on:
- [Knxnet](https://github.com/leadrien/knxnet)
- [PyQt5](https://riverbankcomputing.com/software/pyqt)
Simply execute `python actuasim.py` to start the application.
Once launched, Actuasim will load a file **saved\_rooms.json** containing the building configuration with all the KNX addresses
(if this file does not exist, it is created with a default configuration composed of 2 rooms, each one has 2 blinds and two radiator valves.)
Whenever the application is closed, the building configuration with the current position of blinds and valves are stored in this **saved\_rooms.json** file.
You can edit this it to set up your specific building configuration.
By default, Actuasim is listening on UDP port 3671.
A **actuasim.log** file is written to monitor every received frame or event with the decoded data.
# REPRODUCED HARDWARE
- **Blind**: Tunneling data has to be boolean and will order the blind to move 1=Down or 0=Up
- **Radiator valve**: Tunneling data is one byte, the direct valve position
actuasim.png

32.1 KiB

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import json
import os
import random
import logging
from logging.handlers import RotatingFileHandler
from collections import OrderedDict
from PyQt5.QtWidgets import *
from knxnet import *
from actuasim.room import Room
from actuasim.ui_actuasim import Ui_MainWindow
from actuasim.knxserver import Knxserver
from actuasim.command_handler import CommandHandler
__author__ = "Adrien Lescourt"
__copyright__ = "HES-SO 2015, Project EMG4B"
__credits__ = ["Adrien Lescourt"]
__version__ = "1.0.1"
__email__ = "adrien.lescourt@gmail.com"
__status__ = "Prototype"
class Actuasim(QMainWindow):
save_filename = 'saved_rooms.json'
def __init__(self):
super(Actuasim, self).__init__()
self.logger = logging.getLogger()
self.logger.setLevel(logging.DEBUG)
formatter = logging.Formatter('%(asctime)s :: %(levelname)s :: %(message)s')
file_handler = RotatingFileHandler('actuasim.log', 'a', 10000000, 1)
file_handler.setLevel(logging.DEBUG)
file_handler.setFormatter(formatter)
self.logger.addHandler(file_handler)
self.logger.info('=======================================')
self.logger.info(' ACTUASIM START')
self.logger.info('=======================================')
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
self.resize(1700, 900)
self.classrooms = []
self.tabs = QTabWidget()
self.setCentralWidget(self.tabs)
self.file_menu = self.ui.menubar.addMenu("&File")
self.save_action = QAction("&Save", self, triggered=self.save)
self.file_menu.addAction(self.save_action)
self.load_action = QAction("&Load", self, triggered=self.load)
self.file_menu.addAction(self.load_action)
self.command_handler = CommandHandler(self)
# endpoints, status, id
self.control_endpoint = ('0.0.0.0', 0)
self.data_endpoint = ('0.0.0.0', 0)
self.status = 0
self.channel_id = random.randint(0, 255) # TODO: handle multiple channel
self.tunnel_req_data_response = None # Store the tunneling request that contains the response for a read
# server
self.knxserver = Knxserver()
self.knxserver.trigger.connect(self.frame_received)
self.knxserver.start()
def load_rooms(self, json_data=None):
self._clean_rooms()
if json_data is not None:
data = json.loads(json_data, object_pairs_hook=OrderedDict)
for key, val in data.items():
room = Room.from_dict(title=key, room_dict=val)
self.classrooms.append(room)
self.tabs.addTab(room, key)
def save(self):
classrooms = OrderedDict()
for i in range(self.tabs.count()):
room = self.tabs.widget(i)
classrooms[room.title] = room.get_room_dict()
json_data = json.dumps(classrooms, indent=4)
with open(Actuasim.save_filename, 'w') as f:
f.write(json_data)
def load(self):
if os.path.isfile(Actuasim.save_filename):
with open(Actuasim.save_filename, 'r') as f:
json_data = f.read()
self.load_rooms(json_data)
return True
else:
m = "{0} not found".format(Actuasim.save_filename)
print(m)
logging.error(m)
return False
def load_room_template(self):
template = """
{
"room 1": {
"blinds": [
{
"value": 19,
"address": "1.0.0@1/4/1"
},
{
"value": 19,
"address": "1.0.1@1/4/2"
}
],
"valves": [
{
"value": 19,
"address": "1.0.2@0/4/1"
},
{
"value": 19,
"address": "1.0.3@0/4/2"
}
]
},
"room 2": {
"blinds": [
{
"value": 19,
"address": "2.0.0@1/4/10"
},
{
"value": 19,
"address": "2.0.1@1/4/11"
}
],
"valves": [
{
"value": 19,
"address": "2.0.2@0/4/10"
},
{
"value": 19,
"address": "2.0.3@0/4/11"
}
]
}
}
"""
self.load_rooms(template)
def frame_received(self, frame):
self.logger.info('Frame received:' + str([hex(h) for h in frame[0]]))
decoded_frame = knxnet.decode_frame(frame[0])
if decoded_frame.header.service_type_descriptor == knxnet.ServiceTypeDescriptor.CONNECTION_REQUEST:
self.logger.info('= Connection request:\n' + str(decoded_frame))
conn_resp = knxnet.create_frame(knxnet.ServiceTypeDescriptor.CONNECTION_RESPONSE,
self.channel_id,
self.status,
self.data_endpoint)
self.knxserver.send(conn_resp.frame)
self.logger.info('Frame sent:' + repr(conn_resp))
self.logger.info('= Connection response:\n' + str(conn_resp))
elif decoded_frame.header.service_type_descriptor == knxnet.ServiceTypeDescriptor.CONNECTION_STATE_REQUEST:
self.logger.info('= Connection state request:\n' + str(decoded_frame))
conn_state_resp = knxnet.create_frame(knxnet.ServiceTypeDescriptor.CONNECTION_STATE_RESPONSE,
self.channel_id,
self.status)
self.knxserver.send(conn_state_resp.frame)
self.logger.info('Frame sent:' + repr(conn_state_resp))
self.logger.info('= Connection state response:\n' + str(conn_state_resp))
elif decoded_frame.header.service_type_descriptor == knxnet.ServiceTypeDescriptor.DISCONNECT_REQUEST:
self.logger.info('= Disconnect request:\n' + str(decoded_frame))
disconnect_resp = knxnet.create_frame(knxnet.ServiceTypeDescriptor.DISCONNECT_RESPONSE,
self.channel_id,
self.status)
self.knxserver.send(disconnect_resp.frame)
self.logger.info('Frame sent:' + repr(disconnect_resp))
self.logger.info('= Disconnect response:\n' + str(disconnect_resp))
elif decoded_frame.header.service_type_descriptor == knxnet.ServiceTypeDescriptor.TUNNELLING_REQUEST:
self.logger.info('= Tunnelling request:\n' + str(decoded_frame))
self.tunnel_req_data_response = self.command_handler.handle_tunnelling_request(decoded_frame)
tunnel_ack = knxnet.create_frame(knxnet.ServiceTypeDescriptor.TUNNELLING_ACK,
self.channel_id,
self.status,
decoded_frame.sequence_counter)
self.knxserver.send(tunnel_ack.frame)
self.logger.info('Frame sent:' + repr(tunnel_ack))
self.logger.info('= Tunnelling ack:\n' + str(tunnel_ack))
# after sending our tunnelling ack
# we must send a tunnelling request to confirm the data
data_service = 0x2e
sequence_counter = 0
tunnel_req_confirm = knxnet.create_frame(knxnet.ServiceTypeDescriptor.TUNNELLING_REQUEST,
decoded_frame.dest_addr_group,
decoded_frame.channel_id,
decoded_frame.data,
decoded_frame.data_size,
decoded_frame.apci,
data_service,
sequence_counter)
self.knxserver.send(tunnel_req_confirm.frame)
self.logger.info('Frame sent:' + repr(tunnel_req_confirm))
self.logger.info('= Tunnelling ack:\n' + str(tunnel_req_confirm))
elif decoded_frame.header.service_type_descriptor == knxnet.ServiceTypeDescriptor.TUNNELLING_ACK:
self.logger.info('= Tunnelling ack:\n' + str(decoded_frame))
# Do we have a response to send?
if self.tunnel_req_data_response:
self.knxserver.send(self.tunnel_req_data_response.frame)
self.logger.info('Frame sent:' + repr(self.tunnel_req_data_response))
self.logger.info('= Tunnelling request:\n' + str(self.tunnel_req_data_response))
self.tunnel_req_data_response = None
else:
self.logger.info('The frame is not supported')
def closeEvent(self, QCloseEvent):
self.knxserver.close_server()
self.save()
self.logger.info('=======================================')
self.logger.info(' ***** ACTUASIM STOP *****')
self.logger.info('=======================================')
def _clean_rooms(self):
for i in range(len(self.tabs)):
self.tabs.removeTab(0)
for room in self.classrooms:
room.deleteLater()
self.classrooms = []
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
actuasim = Actuasim()
if not actuasim.load():
msg = "Error loading {0}... Load a default rooms template".format(Actuasim.save_filename)
print(msg)
logging.error(msg)
actuasim.load_room_template()
actuasim.show()
sys.exit(app.exec_())
__author__ = "Adrien Lescourt"
__copyright__ = "HES-SO 2015, Project EMG4B"
__credits__ = ["Adrien Lescourt"]
__version__ = "1.0.1"
__email__ = "adrien.lescourt@gmail.com"
__status__ = "Prototype"
# -*- coding: utf-8 -*-
import logging
from PyQt5.QtWidgets import QWidget
from PyQt5.QtCore import *
from actuasim.ui_blind import Ui_Blind
__author__ = "Adrien Lescourt"
__copyright__ = "HES-SO 2015, Project EMG4B"
__credits__ = ["Adrien Lescourt"]
__version__ = "1.0.1"
__email__ = "adrien.lescourt@gmail.com"
__status__ = "Prototype"
class BlindWidget(QWidget):
def __init__(self, individual_address, group_address, blind_position=0, animation_speed_ms=1500):
super(BlindWidget, self).__init__()
self.ui = Ui_Blind()
self.ui.setupUi(self)
self.logger = logging.getLogger()
self.individual_address = individual_address
self.group_address = group_address
# Progressbar init
self.is_moving = False
self.animation_speed = animation_speed_ms
self.animate_progress = QPropertyAnimation(self.ui.progressBar, QByteArray(bytes('value', 'utf-8')))
self.ui.progressBar.setValue(0)
self.position = blind_position
self.setFixedWidth(220)
self.update_position_value()
self.ui.progressBar.valueChanged.connect(self.update_position_value)
self.animate_progress.finished.connect(self.animation_finished)
# Other stuff init
self.ui.labelBlindAddress.setText(self.address_str)
self.ui.buttonDown.clicked.connect(self.button_down_clicked)
self.ui.buttonUp.clicked.connect(self.button_up_clicked)
@property
def address_str(self):
return str(self.individual_address) + '@' + str(self.group_address)
@property
def position(self):
return self.ui.progressBar.value()
@position.setter
def position(self, value):
if value < 0:
value = 0
if value > 100:
value = 100
self.animate_progressbar(value)
def update_position_value(self):
self.ui.labelPositionValue.setText(str(self.ui.progressBar.value()))
def button_down_clicked(self):
if self.is_moving:
self.animation_finished()
return
self.move_down()
def button_up_clicked(self):
if self.is_moving:
self.animation_finished()
return
self.move_up()
def move_down(self):
self.logger.info('Blind ' + self.address_str + ' DOWN')
self.animation_finished()
self.animate_progressbar(0)
def move_up(self):
self.logger.info('Blind ' + self.address_str + ' UP')
self.animation_finished()
self.animate_progressbar(100)
def move_to(self, value):
self.logger.info('Blind ' + self.address_str + ' MOVE TO %s' % value)
self.animation_finished()
self.animate_progressbar(value)
def animate_progressbar(self, end_value):
if end_value == self.ui.progressBar.value():
return
blind_move_speed_ratio = abs(self.ui.progressBar.value()-end_value)/100
self.animate_progress.setDuration(self.animation_speed*blind_move_speed_ratio)
self.animate_progress.setStartValue(self.ui.progressBar.value())
self.animate_progress.setEndValue(end_value)
self.is_moving = True
self.animate_progress.start()
def animation_finished(self):
self.animate_progress.stop()
self.is_moving = False
# -*- coding: utf-8 -*-
from knxnet import *
__author__ = "Adrien Lescourt"
__copyright__ = "HES-SO 2015, Project EMG4B"
__credits__ = ["Adrien Lescourt"]
__version__ = "1.0.1"
__email__ = "adrien.lescourt@gmail.com"
__status__ = "Prototype"
class CommandHandler:
def __init__(self, actuasim):
self.actuasim = actuasim
self.command_functions = {
0: self._valve_command,
1: self._blind_command,
# 2: self._ask_blind_bool,
3: self._blind_command,
4: self._ask_blind_short
}
def handle_tunnelling_request(self, tunnelling_request):
return self.command_functions[tunnelling_request.dest_addr_group.main_group](tunnelling_request)
def _valve_command(self, tunnelling_request):
valve = self._get_valve_from_group_address(tunnelling_request.dest_addr_group)
if valve is not None:
# read
if tunnelling_request.apci == 0:
data = valve.position
data_size = 2
apci = 1
data_service = 0x29
sequence_counter = 1
tunnel_req_response = knxnet.create_frame(knxnet.ServiceTypeDescriptor.TUNNELLING_REQUEST,
tunnelling_request.dest_addr_group,
tunnelling_request.channel_id,
data,
data_size,
apci,
data_service,
sequence_counter)
return tunnel_req_response
# write
elif tunnelling_request.apci == 2:
valve.position = int(tunnelling_request.data / 0xFF * 100)
else:
self.actuasim.logger.error('Destination address group not found in the simulator: ' +
str(tunnelling_request.dest_addr_group))
def _blind_command(self, tunnelling_request):
blind = self._get_blind_from_group_address(tunnelling_request.dest_addr_group)
if blind is not None:
if tunnelling_request.dest_addr_group.main_group == 3:
value = tunnelling_request.data
blind_value = int(value * (100 / 255)) # [0-255] to [0-100]
blind.move_to(blind_value)
elif tunnelling_request.data == 0:
blind.move_up()
elif tunnelling_request.data == 1:
blind.move_down()
else:
self.actuasim.logger.error('Destination address group not found in the simulator: ' +
str(tunnelling_request.dest_addr_group))
def _ask_blind_short(self, tunnelling_request):
blind = self._get_blind_from_group_address(tunnelling_request.dest_addr_group)
if blind is not None:
data = int(blind.position / 100 * 0xFF)
data_size = 2
apci = 1
data_service = 0x29
sequence_counter = 1
tunnel_req_response = knxnet.create_frame(knxnet.ServiceTypeDescriptor.TUNNELLING_REQUEST,
tunnelling_request.dest_addr_group,
tunnelling_request.channel_id,
data,
data_size,
apci,
data_service,
sequence_counter)
return tunnel_req_response
else:
self.actuasim.logger.error('Destination address group not found in the simulator: ' +
str(tunnelling_request.dest_addr_group))
def _get_valve_from_group_address(self, group_address):
for classroom in self.actuasim.classrooms:
for valve in classroom.valve_list:
if valve.group_address == group_address:
return valve
def _get_blind_from_group_address(self, group_address):
for classroom in self.actuasim.classrooms:
for blind in classroom.blind_list:
# we ignore the action. we do care only about the floor and the block.
if blind.group_address.middle_group == group_address.middle_group \
and blind.group_address.sub_group == group_address.sub_group:
return blind
# -*- coding: utf-8 -*-
import socket
import threading
from PyQt5.QtCore import QObject, pyqtSignal
__author__ = "Adrien Lescourt"
__copyright__ = "HES-SO 2015, Project EMG4B"
__credits__ = ["Adrien Lescourt"]
__version__ = "1.0.1"
__email__ = "adrien.lescourt@gmail.com"
__status__ = "Prototype"
class Knxserver(QObject, threading.Thread):
udp_port = 3671
trigger = pyqtSignal([list])
def __init__(self):
super().__init__()
self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
self.server_running = True
self.addr = None
def run(self):
try:
self.socket.bind(('', self.udp_port))
while self.server_running:
data, self.addr = self.socket.recvfrom(1024)
if data:
if len(data) > len(b'exit'):
self.trigger.emit([data])
finally:
self.socket.close()
def send(self, frame):
if self.server_running:
if self.addr is not None:
self.socket.sendto(frame, self.addr)
def close_server(self):
self.server_running = False
self.socket.sendto(b'exit', ('localhost', self.udp_port))
if __name__ == '__main__':
serv = Knxserver()
serv.start()
\ No newline at end of file
# -*- coding: utf-8 -*-
from PyQt5.QtWidgets import *
from actuasim.blind import BlindWidget
from actuasim.valve import ValveWidget
from knxnet.utils import *
__author__ = "Adrien Lescourt"
__copyright__ = "HES-SO 2015, Project EMG4B"
__credits__ = ["Adrien Lescourt"]
__version__ = "1.0.1"
__email__ = "adrien.lescourt@gmail.com"
__status__ = "Prototype"
class Room(QWidget):
def __init__(self, parent=None, title=None, blind_list=None, valve_list=None):
super(Room, self).__init__(parent)
self.blind_list = []
self.valve_list = []
main_layout = QVBoxLayout()
self.blind_list = blind_list
self.valve_list = valve_list
# blinds
blind_box = QGroupBox("Blinds")
blind_box.setFixedWidth(1670)
blind_box.setFixedHeight(380)
blind_layout = QHBoxLayout()
blind_box.setLayout(blind_layout)
blind_scroll = QScrollArea(self)
blind_scroll.setWidget(blind_box)
for blind in blind_list:
blind_layout.addWidget(blind)
blind_layout.addStretch()
blind_layout.setSpacing(0)
# valves
valve_box = QGroupBox("Valves")
valve_box.setFixedWidth(1670)
valve_box.setFixedHeight(380)
valve_layout = QHBoxLayout()
valve_box.setLayout(valve_layout)
valve_scroll = QScrollArea(self)
valve_scroll.setWidget(valve_box)
for valve in valve_list:
valve_layout.addWidget(valve)
valve_layout.addStretch()
valve_layout.setSpacing(0)
# room
main_layout.addWidget(blind_scroll)
main_layout.addWidget(valve_scroll)
self.setLayout(main_layout)
self.setWindowTitle(title)
self.title = title
@classmethod
def from_dict(cls, title='title', room_dict=None):
blind_list = []
valve_list = []
if room_dict is not None:
for blind in room_dict['blinds']:
addr = blind['address'].split('@')
value = blind['value']
blind_list.append(BlindWidget(IndividualAddress.from_str(addr[0]),
GroupAddress.from_str(addr[1]),
value))
for valve in room_dict['valves']:
addr = valve['address'].split('@')
value = valve['value']
valve_list.append(ValveWidget(IndividualAddress.from_str(addr[0]),
GroupAddress.from_str(addr[1]),
value))
return cls(None, title, blind_list, valve_list)
def get_room_dict(self):
blinds = [{'address': blind.address_str, 'value': blind.position} for blind in self.blind_list]
valves = [{'address': valve.address_str, 'value': valve.position} for valve in self.valve_list]
return {'blinds': blinds, 'valves': valves}
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
room = Room()
room.show()
sys.exit(app.exec_())
\ No newline at end of file
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file 'ui_actuasim.ui'
#
# Created: Tue Jan 27 12:49:30 2015
# by: PyQt5 UI code generator 5.4
#
# WARNING! All changes made in this file will be lost!
from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_MainWindow(object):
def setupUi(self, MainWindow):
MainWindow.setObjectName("MainWindow")
MainWindow.resize(1052, 671)
self.centralwidget = QtWidgets.QWidget(MainWindow)
self.centralwidget.setObjectName("centralwidget")
MainWindow.setCentralWidget(self.centralwidget)
self.menubar = QtWidgets.QMenuBar(MainWindow)
self.menubar.setGeometry(QtCore.QRect(0, 0, 1052, 21))
self.menubar.setObjectName("menubar")
MainWindow.setMenuBar(self.menubar)
self.statusbar = QtWidgets.QStatusBar(MainWindow)
self.statusbar.setObjectName("statusbar")
MainWindow.setStatusBar(self.statusbar)
self.retranslateUi(MainWindow)
QtCore.QMetaObject.connectSlotsByName(MainWindow)
def retranslateUi(self, MainWindow):
_translate = QtCore.QCoreApplication.translate
MainWindow.setWindowTitle(_translate("MainWindow", "Actuasim"))
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1052</width>
<height>671</height>
</rect>
</property>
<property name="windowTitle">
<string>Actuasim</string>
</property>
<widget class="QWidget" name="centralwidget"/>
<widget class="QMenuBar" name="menubar">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1052</width>
<height>21</height>
</rect>
</property>
</widget>
<widget class="QStatusBar" name="statusbar"/>
</widget>
<resources/>
<connections/>
</ui>
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file 'ui_blind.ui'
#
# Created by: PyQt5 UI code generator 5.4.1
#
# WARNING! All changes made in this file will be lost!
from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_Blind(object):
def setupUi(self, Blind):
Blind.setObjectName("Blind")
Blind.resize(275, 294)
self.buttonDown = QtWidgets.QPushButton(Blind)
self.buttonDown.setGeometry(QtCore.QRect(49, 248, 61, 22))
self.buttonDown.setObjectName("buttonDown")
self.progressBar = QtWidgets.QProgressBar(Blind)
self.progressBar.setGeometry(QtCore.QRect(50, 50, 61, 161))
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.progressBar.sizePolicy().hasHeightForWidth())
self.progressBar.setSizePolicy(sizePolicy)
self.progressBar.setProperty("value", 24)
self.progressBar.setTextVisible(True)
self.progressBar.setOrientation(QtCore.Qt.Vertical)
self.progressBar.setObjectName("progressBar")
self.labelPositionValue = QtWidgets.QLabel(Blind)
self.labelPositionValue.setGeometry(QtCore.QRect(11, 25, 141, 17))
font = QtGui.QFont()
font.setPointSize(11)
self.labelPositionValue.setFont(font)
self.labelPositionValue.setText("")
self.labelPositionValue.setAlignment(QtCore.Qt.AlignCenter)
self.labelPositionValue.setObjectName("labelPositionValue")
self.labelBlindAddress = QtWidgets.QLabel(Blind)
self.labelBlindAddress.setGeometry(QtCore.QRect(30, 0, 141, 18))
font = QtGui.QFont()
font.setPointSize(11)
self.labelBlindAddress.setFont(font)
self.labelBlindAddress.setObjectName("labelBlindAddress")
self.buttonUp = QtWidgets.QPushButton(Blind)
self.buttonUp.setGeometry(QtCore.QRect(50, 220, 61, 22))
self.buttonUp.setObjectName("buttonUp")
self.retranslateUi(Blind)
QtCore.QMetaObject.connectSlotsByName(Blind)
def retranslateUi(self, Blind):
_translate = QtCore.QCoreApplication.translate
Blind.setWindowTitle(_translate("Blind", "Form"))
self.buttonDown.setText(_translate("Blind", "v"))
self.labelBlindAddress.setText(_translate("Blind", "5.11.5@1/5/26"))
self.buttonUp.setText(_translate("Blind", "^"))
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Blind</class>
<widget class="QWidget" name="Blind">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>275</width>
<height>294</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<widget class="QPushButton" name="buttonDown">
<property name="geometry">
<rect>
<x>49</x>
<y>248</y>
<width>61</width>
<height>22</height>
</rect>
</property>
<property name="text">
<string>v</string>
</property>
</widget>
<widget class="QProgressBar" name="progressBar">
<property name="geometry">
<rect>
<x>50</x>
<y>50</y>
<width>61</width>
<height>161</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="value">
<number>24</number>
</property>
<property name="textVisible">
<bool>true</bool>
</property>
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
</widget>
<widget class="QLabel" name="labelPositionValue">
<property name="geometry">
<rect>
<x>11</x>
<y>25</y>
<width>141</width>
<height>17</height>
</rect>
</property>
<property name="font">
<font>
<pointsize>11</pointsize>
</font>
</property>
<property name="text">
<string/>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
<widget class="QLabel" name="labelBlindAddress">
<property name="geometry">
<rect>
<x>30</x>
<y>0</y>
<width>141</width>
<height>18</height>
</rect>
</property>
<property name="font">
<font>
<pointsize>11</pointsize>
</font>
</property>
<property name="text">
<string>5.11.5@1/5/26</string>
</property>
</widget>
<widget class="QPushButton" name="buttonUp">
<property name="geometry">
<rect>
<x>50</x>
<y>220</y>
<width>61</width>
<height>22</height>
</rect>
</property>
<property name="text">
<string>^</string>
</property>
</widget>
</widget>
<resources/>
<connections/>
</ui>
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file 'ui_valve.ui'
#
# Created: Mon Mar 9 11:48:33 2015
# by: PyQt5 UI code generator 5.4
#
# WARNING! All changes made in this file will be lost!
from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_Valve(object):
def setupUi(self, Valve):
Valve.setObjectName("Valve")
Valve.resize(274, 233)
self.labelValveAddress = QtWidgets.QLabel(Valve)
self.labelValveAddress.setGeometry(QtCore.QRect(71, 10, 131, 31))
font = QtGui.QFont()
font.setPointSize(11)
self.labelValveAddress.setFont(font)
self.labelValveAddress.setObjectName("labelValveAddress")
self.buttonUp = QtWidgets.QPushButton(Valve)
self.buttonUp.setGeometry(QtCore.QRect(130, 180, 61, 23))
self.buttonUp.setObjectName("buttonUp")
self.buttonDown = QtWidgets.QPushButton(Valve)
self.buttonDown.setGeometry(QtCore.QRect(70, 180, 61, 23))
self.buttonDown.setObjectName("buttonDown")
self.labelPositionValue = QtWidgets.QLabel(Valve)
self.labelPositionValue.setGeometry(QtCore.QRect(90, 40, 75, 18))
font = QtGui.QFont()
font.setPointSize(11)
self.labelPositionValue.setFont(font)
self.labelPositionValue.setAlignment(QtCore.Qt.AlignCenter)
self.labelPositionValue.setObjectName("labelPositionValue")
self.imageOrigin = QtWidgets.QLabel(Valve)
self.imageOrigin.setGeometry(QtCore.QRect(50, 70, 16, 16))
self.imageOrigin.setText("")
self.imageOrigin.setObjectName("imageOrigin")
self.retranslateUi(Valve)
QtCore.QMetaObject.connectSlotsByName(Valve)
def retranslateUi(self, Valve):
_translate = QtCore.QCoreApplication.translate
Valve.setWindowTitle(_translate("Valve", "Form"))
self.labelValveAddress.setText(_translate("Valve", "5.0.5@0/5/26"))
self.buttonUp.setText(_translate("Valve", "+"))
self.buttonDown.setText(_translate("Valve", "-"))
self.labelPositionValue.setText(_translate("Valve", "1"))
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Valve</class>
<widget class="QWidget" name="Valve">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>274</width>
<height>233</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<widget class="QLabel" name="labelValveAddress">
<property name="geometry">
<rect>
<x>71</x>
<y>10</y>
<width>131</width>
<height>31</height>
</rect>
</property>
<property name="font">
<font>
<pointsize>11</pointsize>
</font>
</property>
<property name="text">
<string>5.0.5@0/5/26</string>
</property>
</widget>
<widget class="QPushButton" name="buttonUp">
<property name="geometry">
<rect>
<x>130</x>
<y>180</y>
<width>61</width>
<height>23</height>
</rect>
</property>
<property name="text">
<string>+</string>
</property>
</widget>
<widget class="QPushButton" name="buttonDown">
<property name="geometry">
<rect>
<x>70</x>
<y>180</y>
<width>61</width>
<height>23</height>
</rect>
</property>
<property name="text">
<string>-</string>
</property>
</widget>
<widget class="QLabel" name="labelPositionValue">
<property name="geometry">
<rect>
<x>90</x>
<y>40</y>
<width>75</width>
<height>18</height>
</rect>
</property>
<property name="font">
<font>
<pointsize>11</pointsize>
</font>
</property>
<property name="text">
<string>1</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
<widget class="QLabel" name="imageOrigin">
<property name="geometry">
<rect>
<x>50</x>
<y>70</y>
<width>16</width>
<height>16</height>
</rect>
</property>
<property name="text">
<string/>
</property>
</widget>
</widget>
<resources/>
<connections/>
</ui>
# -*- coding: utf-8 -*-
from math import cos, sin, radians
import os
import logging
from PyQt5.QtWidgets import QWidget
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from actuasim.ui_valve import Ui_Valve
__author__ = "Adrien Lescourt"
__copyright__ = "HES-SO 2015, Project EMG4B"
__credits__ = ["Adrien Lescourt"]
__version__ = "1.0.1"
__email__ = "adrien.lescourt@gmail.com"
__status__ = "Prototype"
class ValveWidget(QWidget):
def __init__(self, individual_address, group_address, valve_position=45, animation_speed_ms=1500):
super(ValveWidget, self).__init__()
self.ui = Ui_Valve()
self.ui.setupUi(self)
self.logger = logging.getLogger()
self.individual_address = individual_address
self.group_address = group_address
self._position = 0
self.position = valve_position
self.setFixedWidth(220)
self.temperature_bar_image = QImage(os.getcwd() + '/res/temperature_bar.png')
self.temperature_bar_rect = QRect(self.ui.imageOrigin.x(),
self.ui.imageOrigin.y(),
150, 85)
self.line_origin = QPoint(self.ui.imageOrigin.x()+75, 135)
self.line_length = 60
self.ui.labelValveAddress.setText(self.address_str)
self.ui.labelPositionValue.setText(str(self._position))
self.ui.buttonDown.clicked.connect(self.button_down_pressed)
self.ui.buttonUp.clicked.connect(self.button_up_pressed)
@property
def address_str(self):
return str(self.individual_address) + '@' + str(self.group_address)
@property
def position(self):
return self._position
@position.setter
def position(self, value):
self._position = value
if self._position < 0:
self._position = 0
if self._position > 100:
self._position = 100
self.logger.info('Valve ' + self.address_str + ' = ' + str(self._position))
self.ui.labelPositionValue.setText(str(self._position))
self.repaint()
def button_down_pressed(self):
self.position -= 1
def button_up_pressed(self):
self.position += 1
def paintEvent(self, event):
painter = QPainter(self)
painter.setRenderHint(int(QPainter.SmoothPixmapTransform) | int(QPainter.Antialiasing))
painter.drawImage(self.temperature_bar_rect, self.temperature_bar_image)
x = self.line_origin.x() + self.line_length * sin(radians((100-self.position)*1.8+90))
y = self.line_origin.y() + self.line_length * cos(radians((100-self.position)*1.8+90))
painter.drawLine(self.line_origin, QPoint(x, y))
res/temperature_bar.png

18.2 KiB

0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment