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
183 lines
7.0 KiB
Python
183 lines
7.0 KiB
Python
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
import logging
|
|
import requests
|
|
import schedule
|
|
import subprocess
|
|
from threading import Thread
|
|
import time
|
|
|
|
from odoo.addons.iot_drivers.tools import certificate, helpers, upgrade, wifi
|
|
from odoo.addons.iot_drivers.tools.system import IS_RPI
|
|
from odoo.addons.iot_drivers.websocket_client import WebsocketClient
|
|
|
|
if IS_RPI:
|
|
from dbus.mainloop.glib import DBusGMainLoop
|
|
DBusGMainLoop(set_as_default=True) # Must be started from main thread
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
drivers = []
|
|
interfaces = {}
|
|
iot_devices = {}
|
|
unsupported_devices = {}
|
|
|
|
|
|
class Manager(Thread):
|
|
ws_channel = ""
|
|
|
|
def __init__(self):
|
|
super().__init__(daemon=True)
|
|
self.identifier = helpers.get_identifier()
|
|
self.domain = self._get_domain()
|
|
self.version = helpers.get_version(detailed_version=True)
|
|
self.previous_iot_devices = {}
|
|
self.previous_unsupported_devices = {}
|
|
|
|
def _get_domain(self):
|
|
"""
|
|
Get the iot box domain based on the IP address and subject.
|
|
"""
|
|
subject = helpers.get_conf('subject')
|
|
ip_addr = helpers.get_ip()
|
|
if subject and ip_addr:
|
|
return ip_addr.replace('.', '-') + subject.strip('*')
|
|
return ip_addr or '127.0.0.1'
|
|
|
|
def _get_changes_to_send(self):
|
|
"""
|
|
Check if the IoT Box information has changed since the last time it was sent.
|
|
Returns True if any tracked property has changed.
|
|
"""
|
|
changed = False
|
|
|
|
current_devices = set(iot_devices.keys()) | set(unsupported_devices.keys())
|
|
previous_devices = set(self.previous_iot_devices.keys()) | set(self.previous_unsupported_devices.keys())
|
|
if current_devices != previous_devices:
|
|
self.previous_iot_devices = iot_devices.copy()
|
|
self.previous_unsupported_devices = unsupported_devices.copy()
|
|
changed = True
|
|
|
|
# IP address change
|
|
new_domain = self._get_domain()
|
|
if self.domain != new_domain:
|
|
self.domain = new_domain
|
|
changed = True
|
|
# Version change
|
|
new_version = helpers.get_version(detailed_version=True)
|
|
if self.version != new_version:
|
|
self.version = new_version
|
|
changed = True
|
|
|
|
return changed
|
|
|
|
@helpers.require_db
|
|
def _send_all_devices(self, server_url=None):
|
|
"""This method send IoT Box and devices information to Odoo database
|
|
|
|
As the server can be down or not started yet (in case of local testing),
|
|
we retry to send the data several times with a delay between each attempt.
|
|
|
|
:param server_url: URL of the Odoo server (provided by decorator).
|
|
"""
|
|
iot_box = {
|
|
'identifier': self.identifier,
|
|
'mac': helpers.get_mac_address(),
|
|
'ip': self.domain,
|
|
'token': helpers.get_token(),
|
|
'version': self.version,
|
|
}
|
|
devices_list = {}
|
|
for device in self.previous_iot_devices.values():
|
|
identifier = device.device_identifier
|
|
devices_list[identifier] = {
|
|
'name': device.device_name,
|
|
'type': device.device_type,
|
|
'manufacturer': device.device_manufacturer,
|
|
'connection': device.device_connection,
|
|
'subtype': device.device_subtype if device.device_type == 'printer' else '',
|
|
}
|
|
devices_list.update(self.previous_unsupported_devices)
|
|
|
|
delay = .5
|
|
max_retries = 5
|
|
for attempt in range(1, max_retries + 1):
|
|
try:
|
|
response = requests.post(
|
|
server_url + "/iot/setup",
|
|
json={'params': {'iot_box': iot_box, 'devices': devices_list}},
|
|
timeout=5,
|
|
)
|
|
response.raise_for_status()
|
|
data = response.json()
|
|
self.ws_channel = data.get('result', '')
|
|
break # Success, exit the retry loop
|
|
except requests.exceptions.RequestException:
|
|
if attempt < max_retries:
|
|
_logger.warning(
|
|
'Could not reach configured server to send all IoT devices, retrying in %s seconds (%d/%d attempts)',
|
|
delay, attempt, max_retries, exc_info=True
|
|
)
|
|
time.sleep(delay)
|
|
else:
|
|
_logger.exception('Could not reach configured server to send all IoT devices after %d attempts.', max_retries)
|
|
except ValueError:
|
|
_logger.exception('Could not load JSON data: Received data is not valid JSON.\nContent:\n%s', response.content)
|
|
break
|
|
|
|
def run(self):
|
|
"""Thread that will load interfaces and drivers and contact the odoo server
|
|
with the updates. It will also reconnect to the Wi-Fi if the connection is lost.
|
|
"""
|
|
if IS_RPI:
|
|
# ensure that the root filesystem is writable retro compatibility (TODO: remove this in 19.0)
|
|
subprocess.run(["sudo", "mount", "-o", "remount,rw", "/"], check=False)
|
|
subprocess.run(["sudo", "mount", "-o", "remount,rw", "/root_bypass_ramdisks/"], check=False)
|
|
|
|
wifi.reconnect(helpers.get_conf('wifi_ssid'), helpers.get_conf('wifi_password'))
|
|
|
|
helpers.start_nginx_server()
|
|
_logger.info("IoT Box Image version: %s", helpers.get_version(detailed_version=True))
|
|
upgrade.check_git_branch()
|
|
|
|
if IS_RPI and helpers.get_odoo_server_url():
|
|
helpers.generate_password()
|
|
|
|
certificate.ensure_validity()
|
|
|
|
# We first add the IoT Box to the connected DB because IoT handlers cannot be downloaded if
|
|
# the identifier of the Box is not found in the DB. So add the Box to the DB.
|
|
self._send_all_devices()
|
|
helpers.download_iot_handlers()
|
|
helpers.load_iot_handlers()
|
|
|
|
for interface in interfaces.values():
|
|
interface().start()
|
|
|
|
# Set scheduled actions
|
|
schedule.every().day.at("00:00").do(certificate.ensure_validity)
|
|
schedule.every().day.at("00:00").do(helpers.reset_log_level)
|
|
schedule.every().day.at("00:00").do(upgrade.check_git_branch)
|
|
|
|
# Set up the websocket connection
|
|
ws_client = WebsocketClient(self.ws_channel)
|
|
if ws_client:
|
|
ws_client.start()
|
|
|
|
# Check every 3 seconds if the list of connected devices has changed and send the updated
|
|
# list to the connected DB.
|
|
while 1:
|
|
try:
|
|
if self._get_changes_to_send():
|
|
self._send_all_devices()
|
|
if IS_RPI and helpers.get_ip() != '10.11.12.1':
|
|
wifi.reconnect(helpers.get_conf('wifi_ssid'), helpers.get_conf('wifi_password'))
|
|
time.sleep(3)
|
|
schedule.run_pending()
|
|
except Exception:
|
|
# No matter what goes wrong, the Manager loop needs to keep running
|
|
_logger.exception("Manager loop unexpected error")
|
|
|
|
|
|
manager = Manager()
|
|
manager.start()
|