class Control: """PID controller with FIWARE interface""" def __init__(self): """Initialization""" # # use envirnment variables # os.environ["CONFIG_FILE"] = "False" # define parameters self.params = {} self.params['controller_name'] = os.getenv("CONTROLLER_NAME", 'PID_1') self.params['type'] = "PID_Controller" self.params['setpoint'] = float(os.getenv("SETPOINT", '293.15')) # reverse mode can be activated by passing negative tunings to controller self.params['Kp'] = float(os.getenv("KP", '1.0')) self.params['Ki'] = float(os.getenv("KI", '0')) self.params['Kd'] = float(os.getenv("KD", '0')) self.params['lim_low'] = float(os.getenv("LIM_LOW", '0')) self.params['lim_upper'] = float(os.getenv("LIM_UPPER", '100')) self.params['pause_time'] = float(os.getenv("PAUSE_TIME", 0.2)) self.params['sensor_entity_name'] = os.getenv("SENSOR_ENTITY_NAME", '') self.params['sensor_type'] = os.getenv("SENSOR_TYPE", None) self.params['sensor_attr'] = os.getenv("SENSOR_ATTR", '') self.params['actuator_entity_name'] = os.getenv( "ACTUATOR_ENTITY_NAME", '') self.params['actuator_type'] = os.getenv("ACTUATOR_TYPE", '') self.params['actuator_command'] = os.getenv("ACTUATOR_COMMAND", '') self.params['actuator_command_value'] = self.params[ 'actuator_command'] + '_info' self.params['service'] = os.getenv("FIWARE_SERVICE", '') self.params['service_path'] = os.getenv("FIWARE_SERVICE_PATH", '') self.params['cb_url'] = os.getenv("CB_URL", "http://localhost:1026") # settings for security mode self.security_mode = os.getenv("SECURITY_MODE", 'False').lower() in ('true', '1', 'yes') self.params['token'] = (None, None) # Get token from keycloak in security mode if self.security_mode: self.kp = KeycloakPython() self.params['token'] = self.kp.get_access_token() # Create simple pid instance self.pid = PID(self.params['Kp'], self.params['Ki'], self.params['Kd'], setpoint=self.params['setpoint'], output_limits=(self.params['lim_low'], self.params['lim_upper'])) # Additional parameters self.auto_mode = True self.u = None # control variable u self.y_act = self.params[ 'setpoint'] # set the initial measurement to set point # Create the fiware header fiware_header = FiwareHeader(service=self.params['service'], service_path=self.params['service_path']) # Create orion context broker client self.ORION_CB = ContextBrokerClient(url=self.params['cb_url'], fiware_header=fiware_header) def create_entity(self): """Creates entitiy of PID controller in orion context broker""" try: self.ORION_CB.get_entity(entity_id=self.params['controller_name'], entity_type=self.params['type']) print('Entity name already assigned') except requests.exceptions.HTTPError as err: msg = err.args[0] if "NOT FOUND" not in msg.upper(): raise # throw other errors except "entity not found" print('[INFO]: Create new PID entity') pid_entity = ContextEntity(id=f"{self.params['controller_name']}", type=self.params['type']) cb_attrs = [] for attr in ['Kp', 'Ki', 'Kd', 'lim_low', 'lim_upper', 'setpoint']: cb_attrs.append( NamedContextAttribute(name=attr, type="Number", value=self.params[attr])) pid_entity.add_attributes(attrs=cb_attrs) self.ORION_CB.post_entity(entity=pid_entity, update=True) def update_params(self): """Read PID parameters of entity in context broker and updates PID control parameters""" # read PID parameters from context broker for attr in ['Kp', 'Ki', 'Kd', 'lim_low', 'lim_upper', 'setpoint']: self.params[attr] = float( self.ORION_CB.get_attribute_value( entity_id=self.params['controller_name'], entity_type=self.params['type'], attr_name=attr)) # update PID parameters self.pid.tunings = (self.params['Kp'], self.params['Ki'], self.params['Kd']) self.pid.output_limits = (self.params['lim_low'], self.params['lim_upper']) self.pid.setpoint = self.params['setpoint'] # read measured values from CB try: # read the current actuator value u (synchronize the value with the actuator) self.u = self.ORION_CB.get_attribute_value( entity_id=self.params['actuator_entity_name'], entity_type=self.params['actuator_type'], attr_name=self.params['actuator_command_value']) if not isinstance(self.u, (int, float)): self.u = None # read the value of process variable y from sensor y = self.ORION_CB.get_attribute_value( entity_id=self.params['sensor_entity_name'], entity_type=self.params['sensor_type'], attr_name=self.params['sensor_attr']) # set 0 if empty if y == " ": y = '0' # convert to float self.y_act = float(y) except requests.exceptions.HTTPError as err: msg = err.args[0] if "NOT FOUND" not in msg.upper(): raise self.auto_mode = False print("Controller connection fails") else: # If no errors are raised self.auto_mode = True # Update the actual actuator value to allow warm star after interruption self.pid.set_auto_mode(self.auto_mode, last_output=self.u) def run(self): """Calculation of PID output""" try: if self.auto_mode: # if connection is good, auto_mode = True -> controller active # calculate PID output self.u = self.pid(self.y_act) # build command command = NamedCommand(name=self.params['actuator_command'], value=round(self.u, 3)) self.ORION_CB.post_command( entity_id=self.params['actuator_entity_name'], entity_type=self.params['actuator_type'], command=command) except requests.exceptions.HTTPError as err: msg = err.args[0] if "NOT FOUND" not in msg.upper(): raise self.auto_mode = False print("Controller connection fails") def update_token(self): """ Update the token if necessary. Write the latest token into the header of CB client. """ token = self.kp.check_update_token_validity( input_token=self.params['token'], min_valid_time=60) if all(token): # if a valid token is returned self.params['token'] = token # Update the header with token self.ORION_CB.headers.update( {"Authorization": f"Bearer {self.params['token'][0]}"}) def control_loop(self): """The control loop""" try: while True: # Update token if run in security mode if self.security_mode: self.update_token() else: pass self.update_params() self.run() time.sleep(self.params['pause_time']) finally: print("control loop fails") os.abort()
# ToDo: create a `opening hours` property and add it to the building object # in the context broker. Do not update the whole entity! In real # scenarios it might have been modified by other users. opening_hours = NamedContextAttribute( name="openingHours", type="array", value=["Mo-Fr 10:00-19:00", "Sa closed", "Su closed"]) cbc.update_or_append_entity_attributes(entity_id=building.id, entity_type=building.type, attrs=[opening_hours]) # ToDo: retrieve and print the opening hours hours = cbc.get_attribute_value(entity_id=building.id, entity_type=building.type, attr_name=opening_hours.name) print(f"Your opening hours: {hours} \n") # ToDo: modify the `opening hours` of the building cbc.update_attribute_value(entity_id=building.id, entity_type=building.type, attr_name=opening_hours.name, value=["Mo-Sa 10:00-19:00", "Su closed"]) # ToDo: At this point you might have already noticed that your local # building model and the building model in the context broker are out of # sync. Hence, synchronize them again! building = cbc.get_entity(entity_id=building.id, entity_type=building.type) # print your building
class ControllerPanel: def __init__(self): # initialize controller parameters (in dict) self.params = self.initialize_params() # FIWARE parameters self.cb_url = os.getenv("CB_URL", "http://localhost:1026") self.entity_id = None # will be read on the web GUI self.entity_type = "PID_Controller" self.service = os.getenv("FIWARE_SERVICE", '') self.service_path = os.getenv("FIWARE_SERVICE_PATH", '') # Create the fiware header fiware_header = FiwareHeader(service=self.service, service_path=self.service_path) # Create orion context broker client self.ORION_CB = ContextBrokerClient(url=self.cb_url, fiware_header=fiware_header) # initial pid controller list self.controller_list = [] try: self.refresh_list() except: pass # initialize gui window sg.theme("DarkBlue") pid_id_bar = [[ sg.Text("Controller ID", size=(10, 1)), sg.Combo(self.controller_list, key="controller_list"), sg.Button("Refresh") ]] param_bars = [[ sg.Text(param.capitalize(), size=(10, 1)), sg.InputText(self.params[param], key=param) ] for param in self.params.keys()] io_bars = [[sg.Button("Send"), sg.Button("Read")]] layout = pid_id_bar + param_bars + io_bars self.window = sg.Window("PID controller", layout, web_port=80, web_start_browser=True) def gui_update(self): """Update the shown text on web GUI""" # update parameter values for param in self.params.keys(): self.window[param].update(self.params[param]) self.window["controller_list"].Update(values=self.controller_list) self.window["controller_list"].Update(value=self.entity_id) def gui_loop(self): """GUI main loop""" try: while True: event, values = self.window.read(timeout=1000) self.entity_id = values["controller_list"] if event in (sg.WINDOW_CLOSED, None): break elif event == "Send": self.send(values) elif event == "Read": print("Read", flush=True) self.read() elif event == "Refresh": self.refresh_list() self.gui_update() finally: print("panel loop fails") self.window.close() os.abort() def read(self): """Read parameter values from context broker""" try: params_update = self.initialize_params() for param in self.params.keys(): params_update[param] = float( self.ORION_CB.get_attribute_value( entity_id=self.entity_id, entity_type=self.entity_type, attr_name=param)) self.params = params_update except requests.exceptions.HTTPError as err: msg = err.args[0] if "NOT FOUND" not in msg.upper(): raise print("Cannot find controller entity") self.params = self.initialize_params() finally: self.gui_update() def send(self, params): """Send new parameter values to context broker""" for param in self.params.keys(): try: value = float(params[param]) self.ORION_CB.update_attribute_value( entity_id=self.entity_id, entity_type=self.entity_type, attr_name=param, value=value) except ValueError: print( f"Wrong value type of {param}: {params[param]}. Must be numeric!" ) def refresh_list(self): """Refresh the controller list""" entity_list = self.ORION_CB.get_entity_list( entity_types=[self.entity_type]) if entity_list: list_new = [controller.id for controller in entity_list] else: list_new = [] if all([isinstance(controller_id, str) for controller_id in list_new]) or not list_new: self.controller_list = list_new @staticmethod def initialize_params(): """Initialize the values of all control parameters""" # initialize controller parameters shown on panel params = { "Kp": "Proportional gain", "Ki": "Integral gain", "Kd": "Derivative gain", "lim_low": "Lower limit of output", "lim_upper": "Upper limit of output", "setpoint": "The set point of control variable" } return params