Includes: - Odoo 19 framework (odoo/) - 27 custom EnCoach addons (new_project/custom_addons/) - encoach_core, encoach_api, encoach_lms_api, encoach_adaptive_api - encoach_exam, encoach_taxonomy, encoach_adaptive, encoach_assignment - encoach_ai, encoach_ai_grading, encoach_ai_generation, encoach_ai_media - encoach_courseware, encoach_communication, encoach_subscription - encoach_notification, encoach_approval, encoach_branding - encoach_classroom, encoach_registration, encoach_stats - encoach_faq, encoach_ticket, encoach_training, encoach_resources - encoach_adaptive_ai, encoach_sis - 21 OpenEduCat Enterprise modules (new_project/enterprise-19/) - 14 OpenEduCat Community modules (new_project/openeducat_erp-19.0/) - Configuration: odoo.conf, requirements.txt, scripts - 200+ REST API endpoints with JWT authentication - SRS and test documentation Made-with: Cursor
202 lines
9.1 KiB
Python
202 lines
9.1 KiB
Python
import json
|
|
import logging
|
|
import platform
|
|
import requests
|
|
import time
|
|
import urllib.parse
|
|
import websocket
|
|
|
|
from threading import Thread
|
|
|
|
from odoo.addons.iot_drivers import main
|
|
from odoo.addons.iot_drivers.tools import helpers, upgrade
|
|
from odoo.addons.iot_drivers.server_logger import close_server_log_sender_handler
|
|
from odoo.addons.iot_drivers.webrtc_client import webrtc_client
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
websocket.enableTrace(True, level=logging.getLevelName(_logger.getEffectiveLevel()))
|
|
|
|
|
|
@helpers.require_db
|
|
def send_to_controller(params, method="send_websocket", server_url=None):
|
|
"""Confirm the operation's completion by sending a response back to the Odoo server
|
|
|
|
:param params: the parameters to send back to the server
|
|
:param method: method to call on the IoT box controller
|
|
:param server_url: URL of the Odoo server (provided by decorator).
|
|
"""
|
|
request_path = f"{server_url}/iot/box/{method}"
|
|
try:
|
|
response = requests.post(request_path, json={'params': params}, timeout=5)
|
|
response.raise_for_status()
|
|
except requests.exceptions.RequestException:
|
|
_logger.exception('Could not reach database URL: %s', request_path)
|
|
|
|
|
|
def on_error(ws, error):
|
|
_logger.error("websocket received an error: %s", error)
|
|
|
|
|
|
@helpers.require_db
|
|
class WebsocketClient(Thread):
|
|
channel = ""
|
|
|
|
def on_open(self, ws):
|
|
"""
|
|
When the client is setup, this function send a message to subscribe to the iot websocket channel
|
|
"""
|
|
self.connect_timestamp = time.monotonic()
|
|
ws.send(json.dumps({
|
|
'event_name': 'subscribe',
|
|
'data': {
|
|
'channels': [self.channel],
|
|
'last': self.last_message_id,
|
|
'identifier': helpers.get_identifier(),
|
|
}
|
|
}))
|
|
|
|
def on_message(self, ws, messages):
|
|
"""Synchronously handle messages received by the websocket."""
|
|
for message in json.loads(messages):
|
|
self.last_message_id = message['id']
|
|
payload = message['message']['payload']
|
|
_logger.info("Received message of type %s", message['message']['type'])
|
|
|
|
if not helpers.get_identifier() in payload.get('iot_identifiers', []):
|
|
continue
|
|
|
|
match message['message']['type']:
|
|
case 'iot_action':
|
|
for device_identifier in payload['device_identifiers']:
|
|
if device_identifier in main.iot_devices:
|
|
start_operation_time = time.perf_counter()
|
|
_logger.info("device '%s' action started", device_identifier)
|
|
main.iot_devices[device_identifier].action(payload)
|
|
_logger.info("device '%s' action finished - %.*f", device_identifier, 3, time.perf_counter() - start_operation_time)
|
|
else:
|
|
# Notify the controller that the device is not connected
|
|
send_to_controller({
|
|
'session_id': payload.get('session_id', '0'),
|
|
'iot_box_identifier': helpers.get_identifier(),
|
|
'device_identifier': device_identifier,
|
|
'status': 'disconnected',
|
|
})
|
|
case 'server_clear':
|
|
if time.monotonic() < self.connect_timestamp + 5.0:
|
|
# This is a hacky way avoid processing an old server_clear message
|
|
# In master we can fix this properly by providing the last message ID to the IoT box on connection
|
|
_logger.warning("Ignoring server_clear message")
|
|
continue
|
|
helpers.disconnect_from_server()
|
|
close_server_log_sender_handler()
|
|
case 'server_update':
|
|
helpers.update_conf({
|
|
'remote_server': payload['server_url']
|
|
})
|
|
helpers.get_odoo_server_url.cache_clear()
|
|
case 'restart_odoo':
|
|
send_to_controller({
|
|
'session_id': payload['session_id'],
|
|
'iot_box_identifier': helpers.get_identifier(),
|
|
'device_identifier': helpers.get_identifier(),
|
|
'status': 'success',
|
|
})
|
|
ws.close()
|
|
helpers.odoo_restart()
|
|
case 'webrtc_offer':
|
|
if not webrtc_client:
|
|
continue
|
|
answer = webrtc_client.offer(payload['offer'])
|
|
send_to_controller({
|
|
'iot_box_identifier': helpers.get_identifier(),
|
|
'answer': answer,
|
|
}, method="webrtc_answer")
|
|
case 'remote_debug':
|
|
if platform.system() == 'Windows':
|
|
continue
|
|
if not payload.get("status"):
|
|
helpers.toggle_remote_connection(payload.get("token", ""))
|
|
time.sleep(1)
|
|
send_to_controller({
|
|
'session_id': 0,
|
|
'iot_box_identifier': helpers.get_identifier(),
|
|
'device_identifier': None,
|
|
'status': 'success',
|
|
'result': {'enabled': helpers.is_ngrok_enabled()}
|
|
})
|
|
case "test_connection":
|
|
send_to_controller({
|
|
'session_id': payload['session_id'],
|
|
'iot_box_identifier': helpers.get_identifier(),
|
|
'device_identifier': helpers.get_identifier(),
|
|
'status': 'success',
|
|
'result': {
|
|
'lan_quality': helpers.check_network(),
|
|
'wan_quality': helpers.check_network("www.odoo.com"),
|
|
}
|
|
})
|
|
case 'bundle_changed':
|
|
# This message is sent by the DB whenever the web JS asset bundle changes.
|
|
# While this is a bit of a hack we use this message to check if the DB has been upgraded,
|
|
# since we know the bundle will always change in this situation.
|
|
upgrade.check_git_branch()
|
|
case _:
|
|
continue
|
|
|
|
def on_close(self, ws, close_status_code, close_msg):
|
|
_logger.debug("websocket closed with status: %s", close_status_code)
|
|
helpers.update_conf({'last_websocket_message_id': self.last_message_id})
|
|
|
|
def __init__(self, channel, server_url=None):
|
|
"""This class will not be instantiated if no db is connected.
|
|
|
|
:param str channel: the channel to subscribe to
|
|
:param str server_url: URL of the Odoo server (provided by decorator).
|
|
"""
|
|
self.channel = channel
|
|
self.last_message_id = int(helpers.get_conf('last_websocket_message_id') or 0)
|
|
self.server_url = server_url
|
|
url_parsed = urllib.parse.urlsplit(server_url)
|
|
scheme = url_parsed.scheme.replace("http", "ws", 1)
|
|
self.websocket_url = urllib.parse.urlunsplit((scheme, url_parsed.netloc, 'websocket', '', ''))
|
|
self.db_name = helpers.get_conf('db_name') or ''
|
|
self.session_id = ''
|
|
super().__init__(daemon=True)
|
|
|
|
def run(self):
|
|
if self.db_name:
|
|
session_response = requests.get(
|
|
self.server_url + "/web/login?db=" + self.db_name,
|
|
allow_redirects=False,
|
|
timeout=10,
|
|
)
|
|
if session_response.status_code in [200, 302]:
|
|
self.session_id = session_response.cookies['session_id']
|
|
else:
|
|
_logger.error("Failed to get session ID, status %s", session_response.status_code)
|
|
|
|
self.ws = websocket.WebSocketApp(self.websocket_url,
|
|
header={"User-Agent": "OdooIoTBox/1.0", "Cookie": f"session_id={self.session_id}"},
|
|
on_open=self.on_open, on_message=self.on_message,
|
|
on_error=on_error, on_close=self.on_close)
|
|
|
|
# The IoT synchronised servers can stop in 2 ways that we need to handle:
|
|
# A. Gracefully:
|
|
# In this case a disconnection signal is sent to the IoT-box
|
|
# The websocket is properly closed, but it needs to be established a new connection when
|
|
# the server will be back.
|
|
#
|
|
# B. Forced/killed:
|
|
# In this case there is no disconnection signal received
|
|
#
|
|
# This will also happen with the graceful quit as `reconnect` will trigger if the server
|
|
# is offline while attempting the new connection
|
|
while True:
|
|
try:
|
|
run_res = self.ws.run_forever(reconnect=10)
|
|
_logger.debug("websocket run_forever return with %s", run_res)
|
|
except Exception:
|
|
_logger.exception("An unexpected exception happened when running the websocket")
|
|
_logger.debug('websocket will try to restart in 10 seconds')
|
|
time.sleep(10)
|