A-Jansen
commited on
Commit
·
4181d9c
1
Parent(s):
9b346a6
add file
Browse files- home.py +141 -0
- oocsi_source.py +438 -0
home.py
ADDED
@@ -0,0 +1,141 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
These are the meta data and instructions
|
3 |
+
Author: Anniek Jansen
|
4 |
+
Course: Human AI interaction
|
5 |
+
|
6 |
+
Instructions:
|
7 |
+
(For Anniek: conda activate py (activate local environment where the right packages are installed))
|
8 |
+
1. Pip install streamlit, oocsi in your python environment
|
9 |
+
2. Save this file somewhere on your computer
|
10 |
+
3. In the command line, cd to where your file is: "cd/...../folder
|
11 |
+
|
12 |
+
4. To run it: streamlit run home.py
|
13 |
+
5. Click on the link it provides you
|
14 |
+
6. You need to click sometimes rerun in the website
|
15 |
+
|
16 |
+
|
17 |
+
To hide menu: copy paste this in config.toml
|
18 |
+
[ui]
|
19 |
+
hideSidebarNav = true
|
20 |
+
|
21 |
+
"""
|
22 |
+
|
23 |
+
|
24 |
+
import streamlit as st
|
25 |
+
import pandas as pd
|
26 |
+
# from oocsi import __init__ as OOCSI
|
27 |
+
from oocsi_source import OOCSI
|
28 |
+
from uuid import uuid4
|
29 |
+
from streamlit_extras.switch_page_button import switch_page
|
30 |
+
import requests
|
31 |
+
import json
|
32 |
+
|
33 |
+
|
34 |
+
|
35 |
+
|
36 |
+
# Check if 'participantID' already exists in session_state
|
37 |
+
# If not, then initialize it
|
38 |
+
if 'participantID' not in st.session_state:
|
39 |
+
st.session_state.participantID= "P" + uuid4().__str__().replace('-', '')[0:10]
|
40 |
+
st.session_state.pages =['SHAP', 'DecisionTree', 'counterfactual', 'visualMap']
|
41 |
+
st.session_state.profileIndices=[25,112]
|
42 |
+
|
43 |
+
if 'oocsi' not in st.session_state:
|
44 |
+
st.session_state.oocsi = OOCSI('','oocsi.id.tue.nl')
|
45 |
+
|
46 |
+
|
47 |
+
# st.write(st.session_state.participantID)
|
48 |
+
|
49 |
+
st.set_page_config(page_title="XAI research", layout="wide")
|
50 |
+
|
51 |
+
header1, header2, header3 = st.columns([1,2,1])
|
52 |
+
consent_form1, consent_form2, consent_form3 = st.columns([1,4,1])
|
53 |
+
|
54 |
+
# with header2:
|
55 |
+
# st.title("Consent form")
|
56 |
+
# st.write("For debugging:")
|
57 |
+
# st.write(st.session_state.participantID)
|
58 |
+
|
59 |
+
with consent_form2:
|
60 |
+
st.header('Information form for participants')
|
61 |
+
st.write('''Hello and thank you for taking the time to participate in this survey.''')
|
62 |
+
st.write('''This document gives you information about the study Comparing Explainable AI (XAI) methods. Before the study begins, it is important that you learn about the procedure followed in this study and that you give your informed consent for voluntary participation. Please read this document carefully. ''')
|
63 |
+
|
64 |
+
st.subheader('Aim and benefit of the study')
|
65 |
+
st.write('''The aim of this study is to measure the satisfaction of users with different types of XAI methods.
|
66 |
+
This information is used to have better understandable/ more satisfying type of explanations in future application. ''')
|
67 |
+
st.write('''This study is performed by Rachel Wang, François Leborgne and Anniek Jansen, all EngD trainees of the Designing Human-System Interaction program and for this course under the supervision of Chao Zhang of the Human-Technology Interaction group.''')
|
68 |
+
|
69 |
+
st.subheader('Procedure')
|
70 |
+
st.markdown('''During this project we ask you to:
|
71 |
+
- Look at different predictions from an AI model (predicting the survival of passengers of the titanic)
|
72 |
+
- Complete a survey at the start of the study with demographic information
|
73 |
+
- Complete a short survey (8 questions) for four types of explanation method
|
74 |
+
- Complete a survey in the end to compare the explanation methods and explain why certain methods had your preference.
|
75 |
+
''')
|
76 |
+
|
77 |
+
st.subheader('Risks')
|
78 |
+
st.markdown("The study does not involve any risks, detrimental side effects, or cause discomfort.")
|
79 |
+
|
80 |
+
st.subheader("Duration")
|
81 |
+
st.markdown("The instructions, measurements and debriefing will take approximately 15 minutes.")
|
82 |
+
|
83 |
+
st.subheader("Voluntary")
|
84 |
+
st.markdown('''Your participation is completely voluntary. You can refuse to participate without giving any reasons and you can stop your participation at any time during the study. You can also withdraw your permission to use your data immediately after completing the study. None of this will have any negative consequences for you whatsoever.''')
|
85 |
+
|
86 |
+
st.subheader("Confidentiality and use, storage, and sharing of data")
|
87 |
+
st.markdown('''
|
88 |
+
All research conducted at the Human-Technology Interaction Group adheres to the Code of Ethics of the NIP (Nederlands Instituut voor Psychologen – Dutch Institute for Psychologists), and this study has been approved by the Ethical Review Board of the department.
|
89 |
+
|
90 |
+
In this study demographic data (gender, age, education level, highest level of education, data literacy, AI expertise), experimental data (response to questionnaires and duration of experiment) will be recorded, analyzed, and stored. The goal of collecting, analyzing, and storing this data is to answer the research question and publish the results in the scientific literature. To protect your privacy, all data that can be used to personally identify you will be stored on an encrypted server of the Human Technology Interaction group for at least 10 years that is only accessible by selected HTI staff members. No information that can be used to personally identify you will be shared with others.
|
91 |
+
|
92 |
+
During the study, the data will be stored on encrypted laptops and DataFoundry - a platform developed by the department of Industrial Design at TU/e and is GDPR compliant. After the analyses, the data will also be made available on OSF (open science framework, a place to share research data open source).
|
93 |
+
|
94 |
+
The data collected in this study might also be of relevance for future research projects within the Human Technology Interaction group as well as for other researchers. The aim of those studies might be unrelated to the goals of this study.
|
95 |
+
The collected data will therefore also be made available to the general public in an online data repository.
|
96 |
+
The coded data collected in this study and that will be released to the public will (to the best of our knowledge and ability) not contain information that can identify you. It will include all answers you provide during the study, including demographic variables (e.g., age and gender) if you choose to provide these during the study.
|
97 |
+
|
98 |
+
At the bottom of this consent form, you can indicate whether or not you agree with participation in this study. You can also indicate whether you agree with the distribution of your data by means of a secured online data repository with open access for the general public and the distribution of your data by means of a secured online data repository with open access for the general public. You are not obliged to letting us use and share your data. If you are not willing to share your data in this way, you can still participate in this study. Your data will be used in the scientific article but not shared with other researchers.
|
99 |
+
|
100 |
+
No video or audio recordings are made that could identify you.
|
101 |
+
|
102 |
+
''')
|
103 |
+
|
104 |
+
st.subheader("Further information")
|
105 |
+
st.markdown('''If you want more information about this study, the study design, or the results, you can contact Anniek Jansen (contact email: [email protected] ).
|
106 |
+
If you have any complaints about this study, please contact the supervisor, Chao Zhang ([email protected]) You can report irregularities related to scientific integrity to confidential advisors of the TU/e.
|
107 |
+
''')
|
108 |
+
|
109 |
+
st.subheader("Informed consent form")
|
110 |
+
st.markdown('''
|
111 |
+
- I am 18 years or older
|
112 |
+
- I have read and understood the information of the corresponding information form for participants.
|
113 |
+
- I have been given the opportunity to ask questions. My questions are sufficiently answered, and I had sufficient time to decide whether I participate.
|
114 |
+
- I know that my participation is completely voluntary. I know that I can refuse to participate and that I can stop my participation at any time during the study, without giving any reasons. I know that I can withdraw permission to use my data directly after the experiment.
|
115 |
+
- I agree to voluntarily participate in this study carried out by the research group Human Technology Interaction and Industrial Design of the Eindhoven University of Technology.
|
116 |
+
- I know that no information that can be used to personally identify me or my responses in this study will be shared with anyone outside of the research team.
|
117 |
+
''')
|
118 |
+
|
119 |
+
OSF= st.radio("I ... (please select below) give permission to make my anonymized recorded data available to others in a public online data repository, and allow others to use this data for future research projects unrelated to this study.",
|
120 |
+
('do',
|
121 |
+
'do not'))
|
122 |
+
|
123 |
+
st.subheader("Consent")
|
124 |
+
agree = st.checkbox('I consent to processing my personal data gathered during the research in the way described in the information sheet.')
|
125 |
+
|
126 |
+
consentforOSF =""
|
127 |
+
if OSF =='do':
|
128 |
+
consentforOSF='yes'
|
129 |
+
else:
|
130 |
+
consentforOSF='no'
|
131 |
+
|
132 |
+
if agree:
|
133 |
+
|
134 |
+
st.write('Thank you! Please continue to the next page to start the experiment')
|
135 |
+
if st.button("Next page"):
|
136 |
+
st.session_state.oocsi.send('EngD_HAII_consent', {
|
137 |
+
'participant_ID': st.session_state.participantID,
|
138 |
+
'consent': 'yes',
|
139 |
+
'consentForOSF': consentforOSF
|
140 |
+
})
|
141 |
+
switch_page("explanationpage")
|
oocsi_source.py
ADDED
@@ -0,0 +1,438 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Copyright (c) 2017-2022 Mathias Funk
|
2 |
+
# This software is released under the MIT License.
|
3 |
+
# http://opensource.org/licenses/mit-license.php
|
4 |
+
|
5 |
+
import json
|
6 |
+
import socket
|
7 |
+
import threading
|
8 |
+
import atexit
|
9 |
+
import time
|
10 |
+
import uuid
|
11 |
+
from math import fsum
|
12 |
+
|
13 |
+
class OOCSI:
|
14 |
+
def __init__(self, handle=None, host='localhost', port=4444, callback=None, logger=None, maxReconnectionAttempts=100000):
|
15 |
+
if handle is None or len(handle.strip()) == 0:
|
16 |
+
self.handle = "OOCSIClient_" + uuid.uuid4().__str__().replace('-', '')[0:15];
|
17 |
+
else:
|
18 |
+
self.handle = handle
|
19 |
+
|
20 |
+
self.receivers = {self.handle: [callback]}
|
21 |
+
self.calls = {}
|
22 |
+
self.services = {}
|
23 |
+
self.reconnect = True
|
24 |
+
self.maxReconnects = maxReconnectionAttempts
|
25 |
+
self.connected = False
|
26 |
+
if logger is not None:
|
27 |
+
self.log = logger
|
28 |
+
|
29 |
+
# Connect the socket to the port where the server is listening
|
30 |
+
self.server_address = (host, port)
|
31 |
+
self.log('connecting to %s port %s' % self.server_address)
|
32 |
+
|
33 |
+
# start the connection
|
34 |
+
# (block till we are connected or connection error/timeout)
|
35 |
+
if not self.connect():
|
36 |
+
self.log('Initial OOCSI connection failed')
|
37 |
+
raise OOCSIDisconnect('OOCSI has not been found')
|
38 |
+
|
39 |
+
# start the connection thread
|
40 |
+
self.runtime = OOCSIThread(self)
|
41 |
+
self.runtime.start()
|
42 |
+
|
43 |
+
def connect(self):
|
44 |
+
connectionSuccessful = False
|
45 |
+
try:
|
46 |
+
# Create a TCP/IP socket
|
47 |
+
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
48 |
+
self.sock.connect(self.server_address)
|
49 |
+
self.sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
50 |
+
|
51 |
+
try:
|
52 |
+
# Send data
|
53 |
+
message = self.handle + '(JSON)'
|
54 |
+
self.internalSend(message)
|
55 |
+
|
56 |
+
data = self.sock.recv(1024).decode()
|
57 |
+
if data.startswith('{'):
|
58 |
+
connectionSuccessful = True
|
59 |
+
self.log('connection established')
|
60 |
+
# re-subscribe for channels
|
61 |
+
for channelName in self.receivers:
|
62 |
+
self.internalSend('subscribe {0}'.format(channelName))
|
63 |
+
self.connected = True
|
64 |
+
elif data.startswith('error'):
|
65 |
+
self.log(data)
|
66 |
+
self.reconnect = False
|
67 |
+
finally:
|
68 |
+
pass
|
69 |
+
except:
|
70 |
+
pass
|
71 |
+
return connectionSuccessful
|
72 |
+
|
73 |
+
def __enter__(self):
|
74 |
+
return self
|
75 |
+
|
76 |
+
def __exit__(self, exc_type, exc_value, traceback):
|
77 |
+
self.stop()
|
78 |
+
|
79 |
+
def log(self, message):
|
80 |
+
print('[{0}]: {1}'.format(self.handle, message))
|
81 |
+
|
82 |
+
def internalSend(self, msg):
|
83 |
+
try:
|
84 |
+
self.sock.sendall((msg + '\n').encode())
|
85 |
+
except:
|
86 |
+
self.connected = False
|
87 |
+
|
88 |
+
def loop(self):
|
89 |
+
try:
|
90 |
+
data = self.sock.recv(4 * 1024 * 1024).decode()
|
91 |
+
lines = data.split("\n")
|
92 |
+
for line in lines:
|
93 |
+
if len(data) == 0:
|
94 |
+
self.sock.close()
|
95 |
+
self.connected = False
|
96 |
+
elif line.startswith('ping') or line.startswith('.'):
|
97 |
+
self.internalSend('.')
|
98 |
+
elif line.startswith('{'):
|
99 |
+
self.receive(json.loads(line))
|
100 |
+
except:
|
101 |
+
pass
|
102 |
+
|
103 |
+
def receive(self, event):
|
104 |
+
sender = event['sender']
|
105 |
+
recipient = event['recipient']
|
106 |
+
|
107 |
+
# clean up the event
|
108 |
+
del event['recipient']
|
109 |
+
del event['sender']
|
110 |
+
del event['timestamp']
|
111 |
+
if 'data' in event:
|
112 |
+
del event['data']
|
113 |
+
|
114 |
+
if '_MESSAGE_HANDLE' in event and event['_MESSAGE_HANDLE'] in self.services:
|
115 |
+
service = self.services[event['_MESSAGE_HANDLE']]
|
116 |
+
del event['_MESSAGE_HANDLE']
|
117 |
+
service(event)
|
118 |
+
self.send(sender, event)
|
119 |
+
self.receiveChannelEvent(sender, recipient, event)
|
120 |
+
|
121 |
+
else:
|
122 |
+
if '_MESSAGE_ID' in event:
|
123 |
+
myCall = self.calls[event['_MESSAGE_ID']]
|
124 |
+
if myCall['expiration'] > time.time():
|
125 |
+
response = self.calls[event['_MESSAGE_ID']]
|
126 |
+
response['response'] = event
|
127 |
+
del response['expiration']
|
128 |
+
del response['_MESSAGE_ID']
|
129 |
+
del response['response']['_MESSAGE_ID']
|
130 |
+
else:
|
131 |
+
del self.calls[event['_MESSAGE_ID']]
|
132 |
+
|
133 |
+
else:
|
134 |
+
self.receiveChannelEvent(sender, recipient, event)
|
135 |
+
|
136 |
+
def receiveChannelEvent(self, sender, recipient, event):
|
137 |
+
if recipient in self.receivers and self.receivers[recipient] != None:
|
138 |
+
for x in self.receivers[recipient]:
|
139 |
+
x(sender, recipient, event)
|
140 |
+
|
141 |
+
def send(self, channelName, data):
|
142 |
+
self.internalSend('sendraw {0} {1}'.format(channelName, json.dumps(data)))
|
143 |
+
|
144 |
+
def call(self, channelName, callName, data, timeout = 1):
|
145 |
+
data['_MESSAGE_HANDLE'] = callName
|
146 |
+
data['_MESSAGE_ID'] = uuid.uuid4().__str__()
|
147 |
+
self.calls[data['_MESSAGE_ID']] = {'_MESSAGE_HANDLE': callName, '_MESSAGE_ID': data['_MESSAGE_ID'], 'expiration': time.time() + timeout}
|
148 |
+
self.send(channelName, data)
|
149 |
+
return self.calls[data['_MESSAGE_ID']]
|
150 |
+
|
151 |
+
|
152 |
+
def callAndWait(self, channelName, callName, data, timeout = 1):
|
153 |
+
call = self.call(channelName, callName, data, timeout)
|
154 |
+
expiration = time.time() + timeout
|
155 |
+
while time.time() < expiration:
|
156 |
+
time.sleep(0.1)
|
157 |
+
if 'response' in call:
|
158 |
+
break;
|
159 |
+
# self.calls.append
|
160 |
+
return call
|
161 |
+
|
162 |
+
def register(self, channelName, callName, callback):
|
163 |
+
self.services[callName] = callback
|
164 |
+
self.internalSend('subscribe {0}'.format(channelName))
|
165 |
+
self.log('registered responder on {0} for {1}'.format(channelName, callName))
|
166 |
+
|
167 |
+
def subscribe(self, channelName, f):
|
168 |
+
if channelName in self.receivers:
|
169 |
+
self.receivers[channelName].append(f)
|
170 |
+
else:
|
171 |
+
self.receivers[channelName] = [f]
|
172 |
+
self.internalSend('subscribe {0}'.format(channelName))
|
173 |
+
self.log('subscribed to {0}'.format(channelName))
|
174 |
+
|
175 |
+
def unsubscribe(self, channelName):
|
176 |
+
del self.receivers[channelName]
|
177 |
+
self.internalSend('unsubscribe {0}'.format(channelName))
|
178 |
+
self.log('unsubscribed from {0}'.format(channelName))
|
179 |
+
|
180 |
+
def variable(self, channelName, key):
|
181 |
+
return OOCSIVariable(self, channelName, key)
|
182 |
+
|
183 |
+
def stop(self):
|
184 |
+
self.reconnect = False
|
185 |
+
self.internalSend('quit')
|
186 |
+
self.sock.close()
|
187 |
+
self.connected = False
|
188 |
+
|
189 |
+
def handleEvent(self, sender, receiver, message):
|
190 |
+
{}
|
191 |
+
|
192 |
+
def returnHandle(self):
|
193 |
+
return self.handle
|
194 |
+
|
195 |
+
def heyOOCSI(self, custom_name=None):
|
196 |
+
if custom_name is None:
|
197 |
+
return (OOCSIDevice(self, self.handle))
|
198 |
+
else:
|
199 |
+
return (OOCSIDevice(self, custom_name))
|
200 |
+
|
201 |
+
|
202 |
+
class OOCSIThread(threading.Thread):
|
203 |
+
def __init__(self, parent=None):
|
204 |
+
self.parent = parent
|
205 |
+
super(OOCSIThread, self).__init__()
|
206 |
+
|
207 |
+
def run(self):
|
208 |
+
|
209 |
+
# register exit handler
|
210 |
+
atexit.register(self._stop)
|
211 |
+
|
212 |
+
# run the established connection
|
213 |
+
while self.parent.connected:
|
214 |
+
self.parent.loop()
|
215 |
+
|
216 |
+
# reconnect
|
217 |
+
if self.parent.reconnect:
|
218 |
+
failedConnectionAttempts = 0
|
219 |
+
while self.parent.reconnect:
|
220 |
+
self.parent.log('re-connecting to OOCSI')
|
221 |
+
if self.parent.connect():
|
222 |
+
failedConnectionAttempts = 0
|
223 |
+
while self.parent.connected:
|
224 |
+
self.parent.loop()
|
225 |
+
else:
|
226 |
+
failedConnectionAttempts += 1
|
227 |
+
time.sleep(5)
|
228 |
+
# raise exception after unsuccessful connection attempts
|
229 |
+
if failedConnectionAttempts == self.parent.maxReconnects:
|
230 |
+
self.parent.log('OOCSI connection failed after 10 attempts')
|
231 |
+
raise OOCSIDisconnect('OOCSI has not been found')
|
232 |
+
|
233 |
+
self.parent.log('closing connection to OOCSI')
|
234 |
+
|
235 |
+
def _stop(self):
|
236 |
+
self.parent.stop()
|
237 |
+
return threading.Thread._stop(self)
|
238 |
+
|
239 |
+
|
240 |
+
|
241 |
+
class OOCSIDisconnect(Exception):
|
242 |
+
pass
|
243 |
+
|
244 |
+
|
245 |
+
|
246 |
+
class OOCSICall:
|
247 |
+
def __init__(self, parent=None):
|
248 |
+
self.uuid = uuid.uuid4()
|
249 |
+
self.expiration = time.time()
|
250 |
+
|
251 |
+
|
252 |
+
|
253 |
+
class OOCSIVariable(object):
|
254 |
+
def __init__(self, oocsi, channelName, key):
|
255 |
+
self.key = key
|
256 |
+
self.channel = channelName
|
257 |
+
oocsi.subscribe(channelName, self.internalReceiveValue)
|
258 |
+
self.oocsi = oocsi
|
259 |
+
self.value = None
|
260 |
+
self.windowLength = 0
|
261 |
+
self.values = []
|
262 |
+
self.minvalue = None
|
263 |
+
self.maxvalue = None
|
264 |
+
self.sigma = None
|
265 |
+
|
266 |
+
def get(self):
|
267 |
+
if self.windowLength > 0 and len(self.values) > 0:
|
268 |
+
return fsum(self.values)/float(len(self.values))
|
269 |
+
else:
|
270 |
+
return self.value
|
271 |
+
|
272 |
+
def set(self, value):
|
273 |
+
tempvalue = value
|
274 |
+
if not self.minvalue is None and tempvalue < self.minvalue:
|
275 |
+
tempvalue = self.minvalue
|
276 |
+
elif not self.maxvalue is None and tempvalue > self.maxvalue:
|
277 |
+
tempvalue = self.maxvalue
|
278 |
+
elif not self.sigma is None:
|
279 |
+
mean = self.get()
|
280 |
+
if not mean is None:
|
281 |
+
if abs(mean - tempvalue) > self.sigma:
|
282 |
+
if mean - tempvalue > 0:
|
283 |
+
tempvalue = mean - self.sigma/float(len(self.values))
|
284 |
+
else:
|
285 |
+
tempvalue = mean + self.sigma/float(len(self.values))
|
286 |
+
|
287 |
+
if self.windowLength > 0:
|
288 |
+
self.values.append(tempvalue)
|
289 |
+
self.values = self.values[-self.windowLength:]
|
290 |
+
else:
|
291 |
+
self.value = tempvalue
|
292 |
+
self.oocsi.send(self.channel, {self.key: value})
|
293 |
+
|
294 |
+
def internalReceiveValue(self, sender, recipient, data):
|
295 |
+
if self.key in data:
|
296 |
+
tempvalue = data[self.key]
|
297 |
+
if not self.minvalue is None and tempvalue < self.minvalue:
|
298 |
+
tempvalue = self.minvalue
|
299 |
+
elif not self.maxvalue is None and tempvalue > self.maxvalue:
|
300 |
+
tempvalue = self.maxvalue
|
301 |
+
elif not self.sigma is None:
|
302 |
+
mean = self.get()
|
303 |
+
if not mean is None:
|
304 |
+
if abs(mean - tempvalue) > self.sigma:
|
305 |
+
if mean - tempvalue > 0:
|
306 |
+
tempvalue = mean - self.sigma/float(len(self.values))
|
307 |
+
else:
|
308 |
+
tempvalue = mean + self.sigma/float(len(self.values))
|
309 |
+
|
310 |
+
if self.windowLength > 0:
|
311 |
+
self.values.append(tempvalue)
|
312 |
+
self.values = self.values[-self.windowLength:]
|
313 |
+
else:
|
314 |
+
self.value = tempvalue
|
315 |
+
|
316 |
+
def min(self, minvalue):
|
317 |
+
self.minvalue = minvalue
|
318 |
+
if self.value < self.minvalue:
|
319 |
+
self.value = self.minvalue
|
320 |
+
return self
|
321 |
+
|
322 |
+
def max(self, maxvalue):
|
323 |
+
self.maxvalue = maxvalue
|
324 |
+
if self.value > self.maxvalue:
|
325 |
+
self.value = self.maxvalue
|
326 |
+
return self
|
327 |
+
|
328 |
+
def smooth(self, windowLength, sigma=None):
|
329 |
+
self.windowLength = windowLength
|
330 |
+
self.sigma = sigma
|
331 |
+
return self
|
332 |
+
|
333 |
+
|
334 |
+
|
335 |
+
class OOCSIDevice():
|
336 |
+
def __init__(self, OOCSI, device_name:str) -> None:
|
337 |
+
self._device_name = device_name
|
338 |
+
self._device = {self._device_name:{}}
|
339 |
+
self._device[self._device_name]["properties"] = {}
|
340 |
+
self._device[self._device_name]["properties"]["device_id"] = OOCSI.returnHandle()
|
341 |
+
self._device[self._device_name]["components"] = {}
|
342 |
+
self._device[self._device_name]["location"] = {}
|
343 |
+
self._components = self._device[self._device_name]["components"]
|
344 |
+
self._oocsi=OOCSI
|
345 |
+
self._oocsi.log(f'Created device {self._device_name}.')
|
346 |
+
|
347 |
+
def addProperty(self, properties:str, propertyValue):
|
348 |
+
self._device[self._device_name]["properties"][properties] = propertyValue
|
349 |
+
self._oocsi.log(f'Added {properties} to the properties list of device {self._device_name}.')
|
350 |
+
return self
|
351 |
+
|
352 |
+
def addLocation(self, location_name:str, latitude:float = 0, longitude:float = 0):
|
353 |
+
self._device[self._device_name]["location"][location_name] = [latitude, longitude]
|
354 |
+
self._oocsi.log(f'Added {location_name} to the locations list of device {self._device_name}.')
|
355 |
+
return self
|
356 |
+
|
357 |
+
def addSensor(self, sensor_name:str, sensor_channel:str, sensor_type:str, sensor_unit:str, sensor_default:float, mode:str = "auto", step:float = None, icon:str = None):
|
358 |
+
self._components[sensor_name]={}
|
359 |
+
self._components[sensor_name]["channel_name"] = sensor_channel
|
360 |
+
self._components[sensor_name]["type"] = "sensor"
|
361 |
+
self._components[sensor_name]["sensor_type"] = sensor_type
|
362 |
+
self._components[sensor_name]["unit"] = sensor_unit
|
363 |
+
self._components[sensor_name]["value"] = sensor_default
|
364 |
+
self._components[sensor_name]["mode"] = mode
|
365 |
+
self._components[sensor_name]["step"] = step
|
366 |
+
self._components[sensor_name]["icon"] = icon
|
367 |
+
self._device[self._device_name]["components"][sensor_name] = self._components[sensor_name]
|
368 |
+
self._oocsi.log(f'Added {sensor_name} to the components list of device {self._device_name}.')
|
369 |
+
return self
|
370 |
+
|
371 |
+
def addNumber(self, number_name:str, number_channel:str, number_min_max, number_unit:str, number_default:float, icon:str = None):
|
372 |
+
self._components[number_name]={}
|
373 |
+
self._components[number_name]["channel_name"] = number_channel
|
374 |
+
self._components[number_name]["min_max"]= number_min_max
|
375 |
+
self._components[number_name]["type"] = "number"
|
376 |
+
self._components[number_name]["unit"] = number_unit
|
377 |
+
self._components[number_name]["value"] = number_default
|
378 |
+
self._components[number_name]["icon"] = icon
|
379 |
+
self._device[self._device_name]["components"][number_name] = self._components[number_name]
|
380 |
+
self._oocsi.log(f'Added {number_name} to the components list of device {self._device_name}.')
|
381 |
+
return self
|
382 |
+
|
383 |
+
def addBinarySensor(self, sensor_name:str, sensor_channel:str, sensor_type:str, sensor_default:bool = False, icon:str = None):
|
384 |
+
self._components[sensor_name]={}
|
385 |
+
self._components[sensor_name]["channel_name"] = sensor_channel
|
386 |
+
self._components[sensor_name]["type"] = "binary_sensor"
|
387 |
+
self._components[sensor_name]["sensor_type"] = sensor_type
|
388 |
+
self._components[sensor_name]["state"] = sensor_default
|
389 |
+
self._components[sensor_name]["icon"] = icon
|
390 |
+
self._device[self._device_name]["components"][sensor_name] = self._components[sensor_name]
|
391 |
+
self._oocsi.log(f'Added {sensor_name} to the components list of device {self._device_name}.')
|
392 |
+
return self
|
393 |
+
|
394 |
+
def addSwitch(self, switch_name:str, switch_channel:str, switch_default:bool = False, icon:str = None):
|
395 |
+
self._components[switch_name]={}
|
396 |
+
self._components[switch_name]["channel_name"] = switch_channel
|
397 |
+
self._components[switch_name]["type"] = "switch"
|
398 |
+
self._components[switch_name]["state"] = switch_default
|
399 |
+
self._components[switch_name]["icon"] = icon
|
400 |
+
self._device[self._device_name]["components"][switch_name] = self._components[switch_name]
|
401 |
+
self._oocsi.log(f'Added {switch_name} to the components list of device {self._device_name}.')
|
402 |
+
return self
|
403 |
+
|
404 |
+
def addLight(self, light_name:str, light_channel:str, led_type:str, spectrum, light_default_state:bool = False, light_default_brightness:int = 0, mired_min_max = None, icon:str = None):
|
405 |
+
SPECTRUM = ["WHITE","CCT","RGB"]
|
406 |
+
LEDTYPE = ["RGB","RGBW","RGBWW","CCT","DIMMABLE","ONOFF"]
|
407 |
+
|
408 |
+
self._components[light_name]={}
|
409 |
+
if led_type in LEDTYPE:
|
410 |
+
if spectrum in SPECTRUM:
|
411 |
+
self._components[light_name]["spectrum"] = spectrum
|
412 |
+
else:
|
413 |
+
self._oocsi.log(f'error, {light_name} spectrum does not exist.')
|
414 |
+
pass
|
415 |
+
else:
|
416 |
+
self._oocsi.log(f'error, {light_name} ledtype does not exist.')
|
417 |
+
pass
|
418 |
+
|
419 |
+
self._components[light_name]["channel_name"] = light_channel
|
420 |
+
self._components[light_name]["type"] = "light"
|
421 |
+
self._components[light_name]["ledType"] = led_type
|
422 |
+
self._components[light_name]["spectrum"] = spectrum
|
423 |
+
self._components[light_name]["min_max"]= mired_min_max
|
424 |
+
self._components[light_name]["state"] = light_default_state
|
425 |
+
self._components[light_name]["brightness"] = light_default_brightness
|
426 |
+
self._components[light_name]["icon"] = icon
|
427 |
+
self._device[self._device_name]["components"][light_name] = self._components[light_name]
|
428 |
+
self._oocsi.log(f'Added {light_name} to the components list of device {self._device_name}.')
|
429 |
+
return self
|
430 |
+
|
431 |
+
def submit(self):
|
432 |
+
data = self._device
|
433 |
+
self._oocsi.internalSend('sendraw {0} {1}'.format("heyOOCSI!", json.dumps(data)))
|
434 |
+
self._oocsi.log(f'Sent heyOOCSI! message for device {self._device_name}.')
|
435 |
+
|
436 |
+
def sayHi(self):
|
437 |
+
self.submit()
|
438 |
+
|