Skip to Content
Odoo Menu
  • Sign in
  • Try it free
  • Apps
    Finance
    • Accounting
    • Invoicing
    • Expenses
    • Spreadsheet (BI)
    • Documents
    • Sign
    Sales
    • CRM
    • Sales
    • POS Shop
    • POS Restaurant
    • Subscriptions
    • Rental
    Websites
    • Website Builder
    • eCommerce
    • Blog
    • Forum
    • Live Chat
    • eLearning
    Supply Chain
    • Inventory
    • Manufacturing
    • PLM
    • Purchase
    • Maintenance
    • Quality
    Human Resources
    • Employees
    • Recruitment
    • Time Off
    • Appraisals
    • Referrals
    • Fleet
    Marketing
    • Social Marketing
    • Email Marketing
    • SMS Marketing
    • Events
    • Marketing Automation
    • Surveys
    Services
    • Project
    • Timesheets
    • Field Service
    • Helpdesk
    • Planning
    • Appointments
    Productivity
    • Discuss
    • Approvals
    • IoT
    • VoIP
    • Knowledge
    • WhatsApp
    Third party apps Odoo Studio Odoo Cloud Platform
  • Industries
    Retail
    • Book Store
    • Clothing Store
    • Furniture Store
    • Grocery Store
    • Hardware Store
    • Toy Store
    Food & Hospitality
    • Bar and Pub
    • Restaurant
    • Fast Food
    • Guest House
    • Beverage Distributor
    • Hotel
    Real Estate
    • Real Estate Agency
    • Architecture Firm
    • Construction
    • Property Management
    • Gardening
    • Property Owner Association
    Consulting
    • Accounting Firm
    • Odoo Partner
    • Marketing Agency
    • Law firm
    • Talent Acquisition
    • Audit & Certification
    Manufacturing
    • Textile
    • Metal
    • Furnitures
    • Food
    • Brewery
    • Corporate Gifts
    Health & Fitness
    • Sports Club
    • Eyewear Store
    • Fitness Center
    • Wellness Practitioners
    • Pharmacy
    • Hair Salon
    Trades
    • Handyman
    • IT Hardware & Support
    • Solar Energy Systems
    • Shoe Maker
    • Cleaning Services
    • HVAC Services
    Others
    • Nonprofit Organization
    • Environmental Agency
    • Billboard Rental
    • Photography
    • Bike Leasing
    • Software Reseller
    Browse all Industries
  • Community
    Learn
    • Tutorials
    • Documentation
    • Certifications
    • Training
    • Blog
    • Podcast
    Empower Education
    • Education Program
    • Scale Up! Business Game
    • Visit Odoo
    Get the Software
    • Download
    • Compare Editions
    • Releases
    Collaborate
    • Github
    • Forum
    • Events
    • Translations
    • Become a Partner
    • Services for Partners
    • Register your Accounting Firm
    Get Services
    • Find a Partner
    • Find an Accountant
      • Get a Tailored Demo
    • Implementation Services
    • Customer References
    • Support
    • Upgrades
    Github Youtube Twitter Linkedin Instagram Facebook Spotify
    +32 2 290 34 90
    • Get a Tailored Demo
  • Pricing
  • Help
  1. APPS
  2. AI
  3. IoT AI Control Hub v 18.0
  4. Sales Conditions FAQ

IoT AI Control Hub

by okkype@gmail.com https://linkedin.com/in/okky-permana-sihipo
Odoo

$ 44.99

v 18.0 Third Party
Apps purchases are linked to your Odoo account, please sign in or sign up first.
Availability
Odoo Online
Odoo.sh
On Premise
Lines of code 2709
Technical Name iotai
LicenseOPL-1
Websitehttps://linkedin.com/in/okky-permana-sihipo
Versions 18.0 19.0
You bought this module and need support? Click here!
Availability
Odoo Online
Odoo.sh
On Premise
Lines of code 2709
Technical Name iotai
LicenseOPL-1
Websitehttps://linkedin.com/in/okky-permana-sihipo
Versions 18.0 19.0
  • Description
  • License

Panduan Lengkap Pembangunan Addon Odoo 18: IoT AI

Panduan langkah-demi-langkah teknis terlengkap untuk membangun sistem otomatisasi pertanian cerdas terintegrasi IoT dan AI lokal pada Odoo 18 Community Edition.

Odoo 18 CE OWL Framework Local AI (GGUF)

Dashboard Hasil Akhir (OWL Frontend)

Komponen antarmuka pengguna ini dibangun menggunakan Odoo Web Library (OWL) dengan visualisasi modern berbasis dark glassmorphism. Dashboard menyediakan pemantauan sensor real-time dengan grafik sparkline interaktif (Chart.js), kontrol relay aktuator secara instan, pemantauan log logis, serta panel rekomendasi bertenaga AI lokal.

Odoo 18 IoT AI Dashboard Preview

📬 Hubungi Kami & Layanan Training

Hubungi kami untuk informasi atau menjadwalkan training

QR WhatsApp 1
WhatsApp Chat 1
+62 857-3257-0386
QR WhatsApp 2
WhatsApp Chat 2
+62 851-6252-3110
Langkah 1

Persiapan Compiler & Paket Sistem Operasi (Build Toolchain)

Untuk menjalankan LLM secara lokal via GGUF, kita mengompilasi llama-cpp-python langsung di mesin host agar mendapatkan akselerasi instruksi perangkat keras CPU/GPU. Pasang dependensi build esensial berikut:

# Perbarui daftar paket dan pasang utilitas compiler
sudo apt update
sudo apt install build-essential python3-dev cmake git fuser -y
Analisis Teknis Mendalam:
  • sudo apt update: Menyinkronkan ulang indeks paket dari repositori sistem operasi untuk memastikan kita mengunduh versi compiler dan library terbaru yang kompatibel dengan kernel Linux host.
  • build-essential: Memasang meta-package compiler C/C++ (GCC, G++, Make, dpkg-dev). Hal ini mutakhir karena parser pustaka GGUF AI lokal (llama.cpp) ditulis menggunakan kode C++ native dan harus di-compile langsung menjadi instruksi biner mesin Linux lokal agar inferensi berjalan dengan performa maksimal.
  • python3-dev: Menyediakan file header C, pustaka link statis/dinamis, dan dependensi sistem untuk membangun ekstensi Python C/C++. Tanpa paket ini, pip akan gagal mengompilasi extension wheel untuk compiler binding Python.
  • cmake: Perkakas otomasi build lintas platform yang bertugas mendeteksi struktur CPU dan membangkitkan Makefile yang optimal untuk instruksi CPU modern (seperti AVX2, AVX512, atau arsitektur ARM Neon).
  • fuser: Utilitas sistem untuk mengidentifikasi proses yang memegang port TCP tertentu. Baris perintah fuser -k 8069/tcp digunakan secara berulang sebelum test runner dijalankan untuk menghentikan instansi Odoo server aktif secara paksa, mencegah bentrokan port TCP yang dapat membatalkan pengujian otomatis.
Langkah 2

Konfigurasi Python Conda Environment

Isolasi environment Python Anda menggunakan Conda untuk menghindari konflik pustaka dengan dependensi bawaan sistem operasi.

# Buat environment baru bernama odoo-18 dengan python 3.10
conda create -n odoo-18 python=3.10 -y

# Aktifkan environment conda tersebut
conda activate odoo-18

# Verifikasi versi python aktif
python --version
Analisis Teknis Mendalam:
  • conda create -n odoo-18 python=3.10 -y: Perintah ini menginstruksikan Conda untuk mengunduh interpreter Python terisolasi versi 3.10. Versi ini adalah target optimal karena memiliki kestabilan runtime API terbaik untuk Odoo 18 serta kecocokan GCC wheel yang aman untuk pustaka LLM lokal. Parameter -y meniadakan dialog interaktif konfirmasi persetujuan pemasangan.
  • conda activate odoo-18: Memodifikasi variabel lingkungan shell (seperti $PATH dan $PYTHONPATH) agar shell secara aktif menunjuk interpreter Python di `/home/user/.conda/envs/odoo-18/bin`. Langkah ini mengisolasi seluruh dependensi pustaka python backend sehingga tidak merusak package manager global Linux (APT).
  • python --version: Langkah verifikasi sanitasi guna memastikan shell telah berhasil di-redirect ke virtual environment yang benar sebelum langkah instalasi pip dijalankan.
Langkah 3

Instalasi Pustaka Tambahan (llama-cpp-python & requests)

Pasang binding Python untuk AI lokal beserta pustaka requests untuk keperluan active sensor polling.

# Instalasi llama-cpp-python dengan compiler wheel host
pip install llama-cpp-python --extra-index-url https://abetlen.github.io/llama-cpp-python/whl/cpu

# Pasang requests untuk HTTP polling
pip install requests
Analisis Teknis Mendalam:
  • pip install llama-cpp-python --extra-index-url...: Perintah ini menargetkan repositori wheel khusus yang telah di-compile sebelumnya untuk komputasi CPU standard. Kompilasi ini memanfaatkan instruksi SIMD (Single Instruction, Multiple Data) CPU untuk memproses operasi aljabar linier tensor AI tanpa memerlukan akselerasi kartu grafis NVIDIA CUDA yang mahal.
  • pip install requests: Memasang library HTTP client Python paling populer. Library ini digunakan oleh backend Odoo untuk mengirim API payload aktif ke mikrokontroler hardware fisik maupun simulator sensor, memproses JSON payload, dan memvalidasi kode respons HTTP secara sinkron.
Langkah 4

Inisialisasi Addon Odoo 18 & Manifest File

Deklarasikan aset CSS, JavaScript (OWL), QWeb XML, data kronologi, dan menu di dalam berkas manifest modul Odoo.

Berkas: __manifest__.py
# -*- coding: utf-8 -*-
{
    'name': 'IoT AI Control Hub',
    'version': '18.0.1.0.0',
    'category': 'IoT/AI',
    'summary': 'AI-driven IoT Automation Controller and Dashboard',
    'description': """
        IoT and AI integration framework for Odoo 18.
        Provides local AI inference using llama-cpp-python (GGUF) and simulated rule engines.
        Includes an OWL dashboard with real-time Chart.js telemetry charts.
    """,
    'depends': ['base', 'web'],
    'data': [
        'security/ir.model.access.csv',
        'data/iotai_cron_data.xml',
        'views/iotai_views.xml',
    ],
    'demo': [
        'demo/iotai_demo_data.xml',
    ],
    'assets': {
        'web.assets_backend': [
            'iotai/static/lib/marked/marked.min.js',
            'iotai/static/src/css/iot_dashboard.css',
            'iotai/static/src/js/iot_dashboard.js',
            'iotai/static/src/xml/iot_dashboard.xml',
        ],
    },
    'installable': True,
    'application': True,
    'license': 'OPL-1',
}
Analisis Teknis Mendalam:
  • 'depends': ['base', 'web']: Menentukan modul inti Odoo sebagai dependensi. Modul web sangat krusial karena menyediakan API aset, kerangka aksi klien (Client Action), JS registry, dan framework OWL backend.
  • 'demo': ['demo/iotai_demo_data.xml']: Memisahkan pemuatan data uji coba (seperti sensor pH dan temperatur default) agar data demo tersebut hanya di-load ketika server dijalankan dengan bendera pengujian (testing) atau database demo baru, menghindari masuknya record sampah ke database produksi asli.
  • 'assets': { 'web.assets_backend': [...] }: Mengintegrasikan file front-end modul ke dalam kompilator aset backend Odoo. Di sini, berkas JS OWL dashboard didaftarkan agar diproses dan di-minify bersama modul-modul backend bawaan Odoo. File `marked.min.js` disertakan secara lokal agar parser Markdown dapat bekerja luring (offline) tanpa dependensi koneksi internet eksternal CDN.
Langkah 5

Pendaftaran Modul & Struktur Folder (__init__.py)

Gunakan berkas inisialisasi Python untuk mendaftarkan folder model dan routing API controller.

Berkas: __init__.py (Root Modul)
# -*- coding: utf-8 -*-
from . import models
from . import controllers
Berkas: models/__init__.py
# -*- coding: utf-8 -*-
from . import sensor
from . import actuator
from . import ai_engine
from . import project_config
Analisis Teknis Mendalam:
  • __init__.py (Root): Titik masuk pertama bagi engine pemuat modul Odoo. Berkas ini bertugas mengekspor folder models (skema data database) dan controllers (route HTTP) agar keduanya dipindai oleh kompiler interpreter Python Odoo saat server di-boot.
  • models/__init__.py: Mengontrol urutan pemuatan model. Python memuat berkas satu demi satu. Pengurutan model dependen seperti sensor dan actuator diletakkan di bagian atas agar model konfigurasi global project_config yang merujuk relasi (Relation Fields) ke kedua tabel tersebut dapat di-compile dengan sukses tanpa memicu error `KeyError` relasi model.
Langkah 6

Desain Model Database IoT (models/sensor.py, models/actuator.py)

Tulis struktur model sensor dan aktuator menggunakan Odoo ORM. Pada model-model ini, kita mengimplementasikan method init() khusus untuk membersihkan data konflik non-XML sebelum instalasi guna menghindari UniqueViolation.

Berkas: models/sensor.py
# -*- coding: utf-8 -*-
from odoo import models, fields, api
import uuid

class IoTAISensor(models.Model):
    _name = "iotai.sensor"
    _description = "IoT AI Sensor"

    key = fields.Char(string="Key", required=True, index=True)
    name = fields.Char(string="Name", required=True)
    sensor_type = fields.Selection([
        ('single', 'Single Value'),
        ('range', 'Range Value')
    ], string="Sensor Type", default='single', required=True)
    current_value = fields.Char(string="Current Value")
    read_mode = fields.Selection([
        ('active', 'Active Polling'),
        ('passive', 'Passive Webhook')
    ], string="Read Mode", default='active', required=True)
    webhook_token = fields.Char(string="Webhook Token", default=lambda self: self._generate_token())
    polling_url = fields.Char(string="Polling URL")
    polling_method = fields.Selection([('GET', 'GET'), ('POST', 'POST')], string="Polling Method", default='GET')
    polling_interval = fields.Integer(string="Polling Interval (Seconds)", default=30)
    polling_headers = fields.Text(string="Polling Headers (JSON)", default="{}")
    polling_payload = fields.Text(string="Polling Payload (JSON)", default="{}")
    json_path = fields.Char(string="JSON Path")
    last_read_time = fields.Datetime(string="Last Read Time")
    is_active = fields.Boolean(string="Active", default=True)
    log_ids = fields.One2many('iotai.sensor.log', 'sensor_id', string="Telemetry Logs")

    _sql_constraints = [('key_uniq', 'unique(key)', 'Sensor key must be unique!')]

    def init(self):
        super().init()
        # Clean up conflicting keys that are not in ir_model_data
        self.env.cr.execute("""
            DELETE FROM iotai_sensor 
            WHERE key IN ('ph_level', 'water_temp', 'ec_level', 'ambient_lux')
              AND id NOT IN (
                  SELECT COALESCE(res_id, 0) FROM ir_model_data WHERE model = 'iotai.sensor'
              )
        """)

    def _generate_token(self):
        return uuid.uuid4().hex[:16]

class IoTAISensorLog(models.Model):
    _name = "iotai.sensor.log"
    _description = "IoT AI Sensor Log"
    _order = "timestamp desc"

    sensor_id = fields.Many2one('iotai.sensor', string="Sensor", required=True, ondelete='cascade')
    value = fields.Char(string="Value", required=True)
    timestamp = fields.Datetime(string="Timestamp", default=fields.Datetime.now, required=True)
Analisis Teknis Mendalam `sensor.py`:
  • key = fields.Char(..., index=True): Memberikan index B-Tree pada kolom basis data PostgreSQL untuk menjamin performa pencarian sensor telemetri secepat O(1) sewaktu menerima request HTTP eksternal.
  • _sql_constraints = [('key_uniq', 'unique(key)', ...)]: Mencegah duplikasi data logis. Kendala tingkat database (PostgreSQL Unique Index) mencegah data inkonsisten jika ada request write konkuren yang mencoba menulis data ganda.
  • def init(self): Metode bawaan Odoo ORM yang dipanggil setelah tabel dibuat/diperbarui. Kita mengeksekusi SQL raw DELETE FROM iotai_sensor WHERE... untuk menyapu record sensor telemetri lama yang kuncinya bertabrakan dengan XML data demo baru. Bagian id NOT IN (SELECT COALESCE(res_id, 0) FROM ir_model_data...) adalah trik arsitektural krusial: ia menjamin record data yang dikelola secara resmi oleh XML Odoo tidak ikut terhapus, sedangkan record sampah sisa seeder Python lama dibersihkan secara tuntas dari tabel.
  • IoTAISensorLog: Dirancang sebagai tabel terpisah untuk menyimpan deret data historis (timeseries data). Atribut _order = "timestamp desc" memastikan data telemetri terbaru selalu berada pada baris teratas, mempercepat query sparkline UI.
Berkas: models/actuator.py
# -*- coding: utf-8 -*-
from odoo import models, fields, api
import uuid

class IoTAIActuator(models.Model):
    _name = "iotai.actuator"
    _description = "IoT AI Actuator"

    key = fields.Char(string="Key", required=True, index=True)
    name = fields.Char(string="Name", required=True)
    description = fields.Text(string="Description")
    state = fields.Selection([('ON', 'ON'), ('OFF', 'OFF')], string="State", default='OFF')
    possible_states = fields.Char(string="Possible States", default="ON, OFF")
    control_mode = fields.Selection([
        ('active', 'Active (Odoo pushes)'),
        ('passive', 'Passive (MCU pulls)')
    ], string="Control Mode", default='active', required=True)
    webhook_token = fields.Char(string="Webhook Token", default=lambda self: self._generate_token())
    api_url = fields.Char(string="API URL")
    api_method = fields.Selection([('GET', 'GET'), ('POST', 'POST')], string="API Method", default='POST')
    api_headers = fields.Text(string="API Headers (JSON)", default="{}")
    api_payload_template = fields.Text(string="API Payload Template (JSON)", default="{}")
    last_action_time = fields.Datetime(string="Last Action Time")
    log_ids = fields.One2many('iotai.actuator.log', 'actuator_id', string="Control Logs")

    _sql_constraints = [('key_uniq', 'unique(key)', 'Actuator key must be unique!')]

    def init(self):
        super().init()
        # Clean up conflicting keys that are not in ir_model_data
        self.env.cr.execute("""
            DELETE FROM iotai_actuator 
            WHERE key IN ('water_pump', 'ph_down_pump', 'nutrient_pump', 'grow_lights')
              AND id NOT IN (
                  SELECT COALESCE(res_id, 0) FROM ir_model_data WHERE model = 'iotai.actuator'
              )
        """)

    def _generate_token(self):
        return uuid.uuid4().hex[:16]

class IoTAIActuatorLog(models.Model):
    _name = "iotai.actuator.log"
    _description = "IoT AI Actuator Log"
    _order = "timestamp desc"

    actuator_id = fields.Many2one('iotai.actuator', string="Actuator", required=True, ondelete='cascade')
    state = fields.Char(string="State", required=True)
    changed_by = fields.Selection([('manual', 'Manual'), ('ai', 'AI Engine')], string="Changed By", default='manual')
    timestamp = fields.Datetime(string="Timestamp", default=fields.Datetime.now, required=True)
Analisis Teknis Mendalam `actuator.py`:
  • control_mode: Membedakan aktuator bertipe Active (Odoo langsung memanggil API HTTP milik aktuator/relay saat status berubah) dan Passive (Odoo hanya menyimpan status target, lalu mikrolokal NAT memanggil endpoint Odoo secara periodik untuk menarik/pull status saklar terbaru).
  • api_payload_template: Menyimpan template JSON string (contoh: {"state": "{state}"}) yang nantinya diganti secara dinamis oleh Odoo sebelum mengirimkan request HTTP POST ke relay mikrokontroler.
  • init(self): Menjalankan pembersihan database PostgreSQL serupa untuk menghapus aktuator demo non-XML agar proses kompilasi database bersih dari entri konflik.
Langkah 7

Konfigurasi Hak Akses Keamanan (ir.model.access.csv)

Konfigurasikan ACL agar model internal dapat diakses oleh grup pengguna Odoo (base.group_user).

Berkas: security/ir.model.access.csv
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_iotai_sensor,access.iotai.sensor,model_iotai_sensor,base.group_user,1,1,1,1
access_iotai_sensor_log,access.iotai.sensor.log,model_iotai_sensor_log,base.group_user,1,1,1,1
access_iotai_actuator,access.iotai.actuator,model_iotai_actuator,base.group_user,1,1,1,1
access_iotai_actuator_log,access.iotai.actuator.log,model_iotai_actuator_log,base.group_user,1,1,1,1
access_iotai_project_config,access.iotai.project.config,model_iotai_project_config,base.group_user,1,1,1,1
access_iotai_ai_decision,access.iotai.ai.decision,model_iotai_ai_decision,base.group_user,1,1,1,1
Analisis Teknis Mendalam:
  • model_id:id: Mereferensikan ID model internal Odoo secara otomatis (contoh: model_iotai_sensor untuk model iotai.sensor). Awalan model_ adalah konvensi penamaan wajib Odoo.
  • group_id:id: Menggunakan base.group_user (Internal User) sebagai batas minimum hak akses pengguna agar staf internal Odoo dapat mengoperasikan IoT Dashboard.
  • 1,1,1,1: Nilai boolean berturut-turut untuk izin Read, Write, Create, dan Delete. Dalam hal ini, semua hak akses diberikan penuh untuk user internal.
Langkah 8

Deklarasi Demo Data Native XML & Sparkline Logs

Hindari programmatic seeder berbasis Python untuk mencegah tabrakan data demo. Gunakan mekanisme deklaratif Odoo XML melalui demo/iotai_demo_data.xml untuk mendaftarkan konfigurasi, sensor, aktuator, serta pre-seeded logs untuk visualisasi grafik.

Berkas: demo/iotai_demo_data.xml
<?xml version="1.0" encoding="utf-8"?>
<odoo>
    <data noupdate="1">
        <!-- Default configuration record -->
        <record id="iotai_project_config_default" model="iotai.project.config">
            <field name="name">SIHIPO Hydroponic Smart Farm</field>
            <field name="description">Sistem otomatisasi pertanian cerdas berbasis IoT dan AI lokal.</field>
            <field name="use_simulated_ai">True</field>
            <field name="is_ai_active">True</field>
            <field name="system_prompt">- IF water_temp IS greater than 28.0 THEN water_pump = ON
- IF water_temp IS less than 22.0 THEN water_pump = OFF
- IF ph_level IS less than 5.5 THEN ph_down_pump = OFF
- IF ph_level IS greater than 6.5 THEN ph_down_pump = ON
- IF ec_level IS less than 1.2 THEN nutrient_pump = ON
- IF ec_level IS greater than 2.0 THEN nutrient_pump = OFF
- IF ambient_lux IS less than 300.0 THEN grow_lights = ON
- IF ambient_lux IS greater than 700.0 THEN grow_lights = OFF</field>
        </record>

        <!-- Sensors -->
        <record id="sensor_ph_level" model="iotai.sensor">
            <field name="key">ph_level</field>
            <field name="name">pH Level Sensor</field>
            <field name="sensor_type">single</field>
            <field name="current_value">6.2</field>
            <field name="read_mode">passive</field>
            <field name="webhook_token">ph_token_123</field>
            <field name="is_active">True</field>
        </record>

        <!-- Seeded Historical Logs for pH Level -->
        <record id="log_ph_level_1" model="iotai.sensor.log">
            <field name="sensor_id" ref="sensor_ph_level"/>
            <field name="value">6.0</field>
        </record>
        <record id="log_ph_level_2" model="iotai.sensor.log">
            <field name="sensor_id" ref="sensor_ph_level"/>
            <field name="value">5.9</field>
        </record>
        <record id="log_ph_level_3" model="iotai.sensor.log">
            <field name="sensor_id" ref="sensor_ph_level"/>
            <field name="value">6.2</field>
        </record>
    </data>
</odoo>
Analisis Teknis Mendalam:
  • noupdate="1": Atribut krusial yang memberi tahu Odoo untuk memuat record data ini HANYA SEKALI saat modul pertama kali dipasang. Jika pengguna memodifikasi parameter konfigurasi atau token webhook lewat UI, pembaruan modul berikutnya tidak akan menimpa (overwrite) perubahan user tersebut.
  • ref="sensor_ph_level": Relasi XML ID Odoo. Menghubungkan log historis secara langsung ke record sensor pH menggunakan kunci relasional eksternal bawaan sistem Odoo, menjamin integritas data referensial.
  • Mekanisme ini sepenuhnya menggantikan fungsi dynamic Python seeding, sehingga data demo bersifat deklaratif, terdokumentasi dengan baik, dan bersih dari tabrakan kunci database saat pengujian unit test.
Langkah 9

Pembuatan Tampilan Backend XML (views/iotai_views.xml)

Definisikan Window Action, Form View, dan Client Action yang mengaitkan tag backend kita ke OWL Frontend.

Berkas: views/iotai_views.xml
<?xml version="1.0" encoding="utf-8"?>
<odoo>
    <!-- Config Form View -->
    <record id="view_iotai_project_config_form" model="ir.ui.view">
        <field name="name">iotai.project.config.form</field>
        <field name="model">iotai.project.config</field>
        <field name="arch" type="xml">
            <form string="IoT AI Configuration">
                <sheet>
                    <group>
                        <field name="name"/>
                        <field name="description"/>
                    </group>
                    <group string="AI Automation Parameters">
                        <group>
                            <field name="use_simulated_ai"/>
                            <field name="model_path" invisible="use_simulated_ai == True"/>
                            <field name="is_ai_active" widget="boolean_toggle"/>
                            <field name="ai_decision_interval"/>
                        </group>
                    </group>
                    <group string="Automation Prompt Rules">
                        <field name="system_prompt" widget="text" placeholder="Add custom state rules..."/>
                    </group>
                </sheet>
            </form>
        </field>
    </record>

    <!-- Client Action untuk Custom OWL Dashboard -->
    <record id="action_iotai_dashboard" model="ir.actions.client">
        <field name="name">IoT AI Control Hub</field>
        <field name="tag">iotai.dashboard</field>
    </record>

    <!-- Menu Utama -->
    <menuitem id="menu_iotai_root" name="IoT AI Control" web_icon="iotai,static/description/icon.png" sequence="10"/>
    <menuitem id="menu_iotai_dashboard" name="Dashboard" parent="menu_iotai_root" action="action_iotai_dashboard" sequence="1"/>
</odoo>
Analisis Teknis Mendalam:
  • invisible="use_simulated_ai == True": Kondisi dinamis (Odoo 18 style). Menyembunyikan inputan path file GGUF jika user memilih opsi "Use Simulated AI (Regex)", menyederhanakan antarmuka administrasi.
  • <field name="tag">iotai.dashboard</field>: Kode pengait utama. String tag ini dicari oleh Odoo JS Registry untuk memetakan dan merender komponen OWL `IoTAIDashboard` saat menu dashboard diklik oleh pengguna.
  • web_icon="iotai,static/description/icon.png": Menentukan ikon navigasi modern untuk app switcher Odoo 18 backend.
Langkah 10

Penjadwalan Latar Belakang Native (ir.cron & Automated Tasks)

Gunakan penjadwal bawaan Odoo (ir.cron) untuk mengotomatisasi pemanggilan active polling sensor dan keputusan AI di latar belakang secara terjadwal.

Berkas: data/iotai_cron_data.xml
<?xml version="1.0" encoding="utf-8"?>
<odoo>
    <record id="ir_cron_poll_active_sensors" model="ir.cron">
        <field name="name">IoT AI: Poll Active Sensors</field>
        <field name="model_id" ref="model_iotai_sensor"/>
        <field name="state">code</field>
        <field name="code">model.action_poll_active_sensors()</field>
        <field name="interval_number">1</field>
        <field name="interval_type">minutes</field>
        <field name="numbercall">-1</field>
        <field name="active">True</field>
    </record>

    <record id="ir_cron_trigger_ai_decision" model="ir.cron">
        <field name="name">IoT AI: Trigger AI Decision Cycle</field>
        <field name="model_id" ref="model_iotai_project_config"/>
        <field name="state">code</field>
        <field name="code">model.action_trigger_ai_decision()</field>
        <field name="interval_number">5</field>
        <field name="interval_type">minutes</field>
        <field name="numbercall">-1</field>
        <field name="active">True</field>
    </record>
</odoo>
Analisis Teknis Mendalam:
  • <field name="state">code</field>: Memberi tahu Odoo bahwa pemicu cron ini akan mengeksekusi blok kode Python terisolasi (sandboxed code).
  • model.action_poll_active_sensors(): Memanggil metode ORM pada model sensor untuk menyapu (loop) semua sensor aktif dan menarik nilainya dari API hardware masing-masing.
  • numbercall = -1: Mengatur cron agar terus berjalan tanpa batas pengulangan (infinite execution).
  • interval_number = 1 dan interval_type = minutes: Menetapkan interval polling sensor aktif berjalan setiap 60 detik sekali.
Langkah 11

Rule-Based Simulated AI Fallback Engine

Tulis modul pembantu Python di models/ai_engine.py untuk memproses logika otomasi berbasis aturan (regex fallback engine) jika server LLM lokal tidak aktif.

Berkas: models/ai_engine.py (Fungsi Evaluasi Simulasi)
# -*- coding: utf-8 -*-
import re
import logging
_logger = logging.getLogger(__name__)

def execute_simulated_inference(system_prompt, sensors, actuators):
    """
    Evaluates rule prompt lines. Example format:
    - IF water_temp IS greater than 28.0 THEN water_pump = ON
    """
    _logger.info("Executing simulated rule-based inference engine")
    commands = []
    lines = system_prompt.split('\n')
    
    # Map key values
    sensor_map = {s['key']: float(s['value']) for s in sensors if s.get('value')}
    
    for line in lines:
        if not line.strip().startswith('- IF'):
            continue
        
        match = re.match(r'-\s*IF\s+(\w+)\s+(IS\s+)?(greater\s+than|less\s+than|equal\s+to)\s+([\d\.]+)\s+THEN\s+(\w+)\s*=\s*(\w+)', line.strip(), re.IGNORECASE)
        if match:
            s_key, _, operator, limit_val, a_key, a_state = match.groups()
            limit_val = float(limit_val)
            
            if s_key in sensor_map:
                current_val = sensor_map[s_key]
                triggered = False
                
                if 'greater' in operator.lower() and current_val > limit_val:
                    triggered = True
                elif 'less' in operator.lower() and current_val 
Analisis Teknis Mendalam:
  • sensor_map = {s['key']: float(s['value'])...}: Membuat dictionary cepat (hashmap) untuk memetakan key sensor ke nilainya dalam tipe data float agar evaluasi numerik akurat.
  • re.match(r'- IF ...'): Regex ekspresi reguler yang kuat untuk mem-parsing sintaks pseudo-code natural. Regex menangkap nama sensor, tipe perbandingan (operator), nilai limit batasan, nama aktuator target, dan state keluaran target.
  • Evaluator ini bertindak sebagai jaring pengaman (fallback engine) yang tangguh. Jika server GGUF/LLM gagal dimuat akibat keterbatasan RAM, sistem akan otomatis beralih ke rule-engine lokal ini untuk menjaga kelangsungan operasional pertanian.
Langkah 12

Singleton Manager Model AI GGUF Lokal

Gunakan instansiasi LLM tunggal (singleton) untuk menghemat ruang memori RAM Odoo server saat memuat model AI GGUF secara lokal.

Berkas: models/ai_engine.py (LLM Cache Manager)
# -*- coding: utf-8 -*-
class LlamaModelManager:
    _llm = None

    @classmethod
    def get_llm(cls, model_path):
        """Loads and caches the Llama GGUF model dynamically."""
        if not cls._llm and model_path:
            try:
                from llama_cpp import Llama
                cls._llm = Llama(
                    model_path=model_path,
                    n_ctx=2048,
                    n_threads=4,
                    verbose=False
                )
            except Exception as e:
                _logger.error("Failed to initialize Llama-CPP-Python: %s", str(e))
                cls._llm = None
        return cls._llm

def execute_gguf_inference(model_path, system_prompt, prompt):
    """Runs local inference using local GGUF cache manager."""
    llm = LlamaModelManager.get_llm(model_path)
    if not llm:
        return {'status': 'error', 'response': 'Model not loaded'}
        
    full_prompt = f"System: {system_prompt}\nUser: {prompt}\nResponse:"
    result = llm(full_prompt, max_tokens=150, stop=["\n", "User:"])
    text_res = result['choices'][0]['text'].strip()
    return {
        'status': 'success',
        'response': text_res
    }
Analisis Teknis Mendalam:
  • _llm = None: Variabel privat kelas (class private variable) untuk menyimpan instance Llama CPP yang telah dimuat ke dalam memori RAM RAM.
  • get_llm(cls, model_path): Implementasi pola desain **Singleton Pattern**. Model GGUF yang berukuran gigabyte hanya akan dibaca dari harddisk dan dimuat ke RAM SEKALI saja saat pemanggilan pertama. Pemanggilan berikutnya akan memakai cache memori yang sudah aktif, mencegah Odoo kekehabisan RAM.
  • n_threads=4: Menginstruksikan modul AI untuk mengeksekusi inferensi secara paralel memanfaatkan 4 core CPU fisik demi kecepatan respons instruksi.
Langkah 13

Membangun Controller Webhook Terproteksi Token

ESP32/MCU dapat mengirim data ke Odoo melalui route HTTP. Kita menggunakan kelas http.Controller dengan auth="public". Untuk menulis ke database Odoo sebagai user publik, kita wajib menggunakan .sudo() agar tidak terkena hambatan ACL security.

Berkas: controllers/main.py (Webhook Endpoint)
# -*- coding: utf-8 -*-
from odoo import http, fields
from odoo.http import request
import json
import logging
_logger = logging.getLogger(__name__)

class IoTAIController(http.Controller):

    @http.route('/api/sensor/<string:key>', type='http', auth='public', methods=['POST', 'PUT'], csrf=False)
    def sensor_webhook(self, key, **kwargs):
        # Gunakan sudo() karena dipanggil oleh user publik tanpa login session
        sensor = request.env['iotai.sensor'].sudo().search([
            ('key', '=', key), 
            ('read_mode', '=', 'passive')
        ], limit=1)
        
        token = request.httprequest.args.get('token')
        if not sensor or token != sensor.webhook_token:
            return request.make_response(
                json.dumps({'error': 'Unauthorized or invalid sensor key'}), 
                status=401,
                headers=[('Content-Type', 'application/json')]
            )

        val = request.params.get('value') or request.get_json_data().get('value')
        if not val:
            return request.make_response(
                json.dumps({'error': 'Missing value field'}), 
                status=400,
                headers=[('Content-Type', 'application/json')]
            )

        # Update telemetry data
        sensor.write({
            'current_value': str(val),
            'last_read_time': fields.Datetime.now()
        })
        
        request.env['iotai.sensor.log'].sudo().create({
            'sensor_id': sensor.id,
            'value': str(val),
            'timestamp': fields.Datetime.now()
        })

        _logger.info("Sensor '%s' updated via webhook: %s", key, val)
        return request.make_response(
            json.dumps({'status': 'success', 'key': key, 'value': val}), 
            headers=[('Content-Type', 'application/json')]
        )
Analisis Teknis Mendalam:
  • auth='public' & csrf=False: Endpoint dibuat publik agar mikrokontroler (ESP32/Arduino) yang memiliki memori kecil tidak perlu mengelola otentikasi login berbasis cookie Odoo dan dapat mengirim POST request tanpa token CSRF.
  • token = request.httprequest.args.get('token'): Menyeimbangkan keamanan. Meskipun rute bersifat publik, data masuk tetap diverifikasi secara ketat menggunakan query token rahasia yang unik per sensor.
  • request.env[...].sudo(): Pola desain krusial Odoo. Karena request dikirim tanpa autentikasi user backend Odoo, pemanggilan database wajib menggunakan konteks superuser (sudo) agar tidak memicu error pelanggaran hak akses ACL (Access Control List).
Langkah 14

Implementasi Kontrol Status Aktuator Pasif (MCU Pull-Based)

Sediakan rute API GET /api/actuator/<key>/status agar ESP32 yang berada di balik jaringan NAT/Router lokal tetap dapat menarik instruksi saklar ON/OFF terbaru secara aman.

Berkas: controllers/main.py (Actuator Pull)
    @http.route('/api/actuator/<string:key>/status', type='http', auth='public', methods=['GET'], csrf=False)
    def actuator_status_api(self, key, **kwargs):
        actuator = request.env['iotai.actuator'].sudo().search([
            ('key', '=', key), 
            ('control_mode', '=', 'passive')
        ], limit=1)
        
        token = request.httprequest.args.get('token')
        if not actuator or token != actuator.webhook_token:
            return request.make_response(
                json.dumps({'error': 'Unauthorized'}), 
                status=401,
                headers=[('Content-Type', 'application/json')]
            )

        return request.make_response(
            json.dumps({
                'key': actuator.key,
                'state': actuator.state or 'OFF'
            }), 
            headers=[('Content-Type', 'application/json')]
        )
Analisis Teknis Mendalam:
  • Endpoint ini didesain khusus untuk mengatasi masalah **NAT Traversal**. Banyak mikrokontroler di lapangan tidak memiliki alamat IP publik statis sehingga Odoo tidak bisa memicu HTTP POST langsung ke arah mereka.
  • Dengan pola pull-based status ini, mikrokontroler bertindak sebagai inisiator koneksi (client) yang secara periodik mengirimkan GET request ke server Odoo untuk mengetahui apakah status saklarnya (misal: LED Grow Light) diinstruksikan ON atau OFF oleh AI Odoo.
Langkah 15

Dashboard OWL Frontend & Integrasi Chart.js

Tulis komponen frontend menggunakan OWL (Odoo Web Library). Kita mengimplementasikan dinamika penting berikut:

  • Dynamic Library Loading: Menggunakan loadBundle("web.chartjs_lib") dalam lifecycle onWillStart untuk memuat library visualisasi bawaan Odoo secara aman.
  • HTML Markup Rendering: Mengimpor markup dari @odoo/owl untuk membungkus hasil parser Markdown (dari library marked) sehingga tag HTML dapat di-render secara unescaped oleh directive t-out.
  • Bypass SCSS Compilation Limit: Odoo mengompilasi dan menggabungkan semua file CSS di web.assets_backend. Memasukkan `@import` di dalam CSS modul akan merusak parser aset karena letaknya bergeser ke tengah bundle. Kami mengatasinya dengan memuat Google Fonts secara dinamis menggunakan tag HTML <link> di dalam QWeb XML.
Berkas: static/src/js/iot_dashboard.js (OWL Core)
/* @odoo-module */
import { Component, onMounted, onWillStart, onWillUnmount, useState, markup } from "@odoo/owl";
import { registry } from "@web/core/registry";
import { useService } from "@web/core/utils/hooks";
import { loadBundle } from "@web/core/assets";

export class IoTAIDashboard extends Component {
    setup() {
        this.orm = useService("orm");
        this.notification = useService("notification");
        this.state = useState({
            project: { 
                name: "Loading...", 
                sensors: [], 
                actuators: [], 
                ai_decisions: [] 
            },
            aiInsightHtml: "",
            isGeneratingInsight: false,
            isPolling: false
        });
        this.charts = {};

        onWillStart(async () => {
            // Memuat Chart.js bawaan Odoo secara dinamis
            await loadBundle("web.chartjs_lib");
        });

        onMounted(() => {
            this.loadData();
            // Polling data telemetri real-time setiap 3 detik
            this.pollInterval = setInterval(() => this.loadData(true), 3000);
        });

        onWillUnmount(() => {
            clearInterval(this.pollInterval);
            // Bersihkan instansi Chart.js untuk mencegah kebocoran memori (memory leak) di browser
            Object.values(this.charts).forEach(chart => chart.destroy());
        });
    }

    async loadData(isSilent = false) {
        try {
            const res = await this.orm.call(
                "iotai.project.config",
                "action_get_realtime_data",
                [[]]
            );
            Object.assign(this.state.project, res);
            this.renderCharts();
        } catch (error) {
            if (!isSilent) {
                console.error("Failed to load dashboard data:", error);
                this.notification.add("Connection lost to Odoo IoT backend.", { type: "danger" });
            }
        }
    }

    async onPollSensors() {
        this.state.isPolling = true;
        try {
            await this.orm.call("iotai.sensor", "action_poll_active_sensors", [[]]);
            this.notification.add("Successfully polled active sensors.", { type: "success" });
            await this.loadData();
        } catch (error) {
            this.notification.add("Sensor polling request failed.", { type: "danger" });
        } finally {
            this.state.isPolling = false;
        }
    }

    async onToggleActuator(key, currentState) {
        const targetState = currentState === "ON" ? "OFF" : "ON";
        try {
            await this.orm.call(
                "iotai.actuator",
                "action_set_state_by_key",
                [[], key, targetState]
            );
            this.notification.add(`Actuator ${key} set to ${targetState}`, { type: "success" });
            await this.loadData();
        } catch (error) {
            this.notification.add("Failed to update actuator state.", { type: "danger" });
        }
    }

    async onGenerateInsights() {
        this.state.isGeneratingInsight = true;
        try {
            const res = await this.orm.call(
                "iotai.project.config",
                "action_generate_ai_insight",
                [[]]
            );
            if (res && res.status === "success") {
                // Konversi mentah teks Markdown ke HTML lalu dibungkus dengan markup()
                this.state.aiInsightHtml = markup(marked.parse(res.insight));
                this.notification.add("AI analysis completed.", { type: "success" });
            } else {
                this.notification.add(res.message || "Failed to parse AI response.", { type: "warning" });
            }
        } catch (error) {
            this.notification.add("Local inference server offline.", { type: "danger" });
        } finally {
            this.state.isGeneratingInsight = false;
        }
    }

    renderCharts() {
        this.state.project.sensors.forEach(sensor => {
            const canvasId = `chart_${sensor.key}`;
            const canvas = document.getElementById(canvasId);
            if (!canvas) return;

            const logs = sensor.history_logs || [];
            const labels = logs.map((_, idx) => idx + 1);
            const dataPoints = logs.map(l => parseFloat(l));

            if (this.charts[sensor.key]) {
                const chart = this.charts[sensor.key];
                chart.data.labels = labels;
                chart.data.datasets[0].data = dataPoints;
                chart.update("none"); // Update grafik secara langsung tanpa efek animasi lambat redundan
            } else {
                const ctx = canvas.getContext("2d");
                this.charts[sensor.key] = new Chart(ctx, {
                    type: "line",
                    data: {
                        labels: labels,
                        datasets: [{
                            data: dataPoints,
                            borderColor: "#00A09D",
                            backgroundColor: "rgba(0, 160, 157, 0.05)",
                            borderWidth: 2,
                            pointRadius: 0,
                            fill: true,
                            tension: 0.3
                        }]
                    },
                    options: {
                        responsive: true,
                        maintainAspectRatio: false,
                        plugins: { legend: { display: false } },
                        scales: {
                            x: { display: false },
                            y: { grid: { color: "rgba(255, 255, 255, 0.05)" }, ticks: { color: "#94a3b8" } }
                        }
                    }
                });
            }
        });
    }
}
IoTAIDashboard.template = "iotai.IoTAIDashboard";
registry.category("actions").add("iotai.dashboard", IoTAIDashboard);
Analisis Teknis Mendalam Frontend OWL JS:
  • setup(): Konstruktor OWL modern. Di sini kita memanggil useService("orm") dan useService("notification") untuk menginisialisasi RPC engine bawaan Odoo dan toaster panel info pop-up.
  • useState: Hook reaktivitas OWL. Komponen otomatis merender ulang elemen HTML dashboard jika properti di dalam state (seperti nilai sensor baru atau status saklar relay) mengalami perubahan data.
  • loadBundle("web.chartjs_lib"): Memastikan pustaka Chart.js bawaan sistem Odoo diunduh secara asinkron ke browser hanya saat pengguna mengakses aplikasi IoT. Ini menjaga ukuran aset inti tetap ringan (Web Performance Optimization).
  • markup(marked.parse(...)): Fungsi pengaman esensial OWL. OWL secara bawaan menolak merender variabel yang berisi sintaks HTML (untuk keamanan XSS Injection). Kita memberi tahu OWL secara eksplisit dengan membungkus HTML parser Markdown menggunakan helper markup() agar dapat dirender menggunakan tag QWeb t-out.
  • chart.update("none"): Menggunakan parameter pembaruan tanpa animasi untuk menghemat kinerja komputasi kartu grafis/CPU pengguna saat telemetri sensor berganti nilai setiap 3 detik.
  • onWillUnmount(): Hook siklus hidup OWL yang mendeteksi saat user berpindah menu navigasi. Di sini kita menghapus timer interval pollInterval dan memanggil chart.destroy() pada semua canvas. Langkah ini adalah **best-practice mutlak** untuk mencegah kebocoran memori (JS memory leaks) yang dapat memperlambat browser pengguna.
Odoo Proprietary License v1.0

This software and associated files (the "Software") may only be used (executed,
modified, executed after modifications) if you have purchased a valid license
from the authors, typically via Odoo Apps, or if you have received a written
agreement from the authors of the Software (see the COPYRIGHT file).

You may develop Odoo modules that use the Software as a library (typically
by depending on it, importing it and using its resources), but without copying
any source code or material from the Software. You may distribute those
modules under the license of your choice, provided that this license is
compatible with the terms of the Odoo Proprietary License (For example:
LGPL, MIT, or proprietary licenses similar to this one).

It is forbidden to publish, distribute, sublicense, or sell copies of the Software
or modified copies of the Software.

The above copyright notice and this permission notice must be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.

Please log in to comment on this module

  • The author can leave a single reply to each comment.
  • This section is meant to ask simple questions or leave a rating. Every report of a problem experienced while using the module should be addressed to the author directly (refer to the following point).
  • If you want to start a discussion with the author or have a question related to your purchase, please use the support page.
Community
  • Tutorials
  • Documentation
  • Forum
Open Source
  • Download
  • Github
  • Runbot
  • Translations
Services
  • Odoo.sh Hosting
  • Support
  • Upgrade
  • Custom Developments
  • Education
  • Find an Accountant
  • Find a Partner
  • Become a Partner
About us
  • Our company
  • Brand Assets
  • Contact us
  • Jobs
  • Events
  • Podcast
  • Blog
  • Customers
  • Legal • Privacy
  • Security

Odoo is a suite of open source business apps that cover all your company needs: CRM, eCommerce, accounting, inventory, point of sale, project management, etc.

Odoo's unique value proposition is to be at the same time very easy to use and fully integrated.

Website made with