Ciao, mi piacerebbe creare delle UI in PHP forse con Yii2 per gestire la domotica, auto, batterie domestiche … Potrei installare il tutto in uno spazio web in cloud che ho già in Serverplan. Ma creare le ui è molto difficile, forse integrare con js, ma questo rende il tutto ancora più difficile, potresti darmi qualche aiuto?
Ciao! Creare interfacce di gestione domotica in PHP/Yii2 è una buona idea, e posso aiutarti a semplificare il processo. Ecco alcuni approcci e consigli:
1. Architettura Semplificata
text
Yii2 (Backend PHP) + Bootstrap (Frontend) + AJAX (Comunicazione)
2. Componenti Pronti da Usare
Per Yii2:
php
// Estensione per dashboard composer require hayageek/yii2-aws-s3 // per storage composer require 2amigos/yii2-chartjs-widget // per grafici
Alternative più semplici:
- AdminLTE per Yii2: Template admin già pronto
- Gentelella: Altro template completo
- Yii2-app-advanced: Struttura già organizzata
3. Esempio Base per Monitoraggio Batterie
php
// Controller
namespace app\controllers;
use yii\web\Controller;
class DomoticaController extends Controller
{
public function actionDashboard()
{
// Dati esempio - poi collegherai a database/API
$batterie = [
'carica' => 85,
'temperatura' => 25,
'stato' => 'Caricando'
];
return $this->render('dashboard', [
'batterie' => $batterie
]);
}
public function actionGetData()
{
\Yii::$app->response->format = \yii\web\Response::FORMAT_JSON;
return [
'batteria' => rand(70, 100),
'consumo' => rand(1, 10),
'timestamp' => date('H:i:s')
];
}
}
html
<!-- View dashboard.php -->
<div class="row">
<div class="col-md-4">
<div class="panel panel-info">
<div class="panel-heading">Batteria Casa</div>
<div class="panel-body">
<div class="progress">
<div class="progress-bar" role="progressbar"
style="width: <?= $batterie['carica'] ?>%;">
<?= $batterie['carica'] ?>%
</div>
</div>
<p>Temperatura: <?= $batterie['temperatura'] ?>°C</p>
<p>Stato: <?= $batterie['stato'] ?></p>
</div>
</div>
</div>
</div>
<!-- Aggiornamento AJAX semplice -->
<script>
setInterval(function() {
$.get('/domotica/get-data', function(data) {
$('.progress-bar').css('width', data.batteria + '%')
.text(data.batteria + '%');
});
}, 5000); // Ogni 5 secondi
</script>
4. Strumenti che Semplificano
a) Freeboard (Open Source)
- Dashboard già pronta
- Si integra con PHP come backend
- Solo configurazione, zero codice JS
b) Grafana + Plugin MySQL
- Per grafici complessi
- Yii2 salva dati su DB, Grafana visualizza
c) Home Assistant (in PHP)
Framework leggero che puoi ispirarti:
php
// Struttura semplice per dispositivi
class Device {
public $name;
public $value;
public $type; // switch, sensor, dimmer
public function toArray() {
return [
'name' => $this->name,
'value' => $this->value,
'type' => $this->type
];
}
}
5. Connessione ai Dispositivi
Metodo Semplice: REST API
php
// Yii2 come hub centrale
public function actionSwitchLight($deviceId, $state)
{
$device = Device::findOne($deviceId);
// Invia comando (esempio con ESP8266)
$ch = curl_init("http://{$device->ip}/switch");
curl_setopt($ch, CURLOPT_POSTFIELDS, ['state' => $state]);
curl_exec($ch);
return $this->redirect(['dashboard']);
}
6. Database Structure Semplice
sql
CREATE TABLE devices (
id INT PRIMARY KEY,
name VARCHAR(100),
type ENUM('light', 'socket', 'sensor'),
ip VARCHAR(15),
status BOOLEAN,
last_update TIMESTAMP
);
CREATE TABLE energy_logs (
id INT PRIMARY KEY AUTO_INCREMENT,
device_id INT,
consumption FLOAT,
timestamp DATETIME
);
7. Deploy sul tuo ServerPlan
Configurazione minima:
- PHP 7.4+ con estensioni: JSON, cURL
- MySQL 5.7+
- Cron jobs per aggiornamenti automatici
.htaccess per Yii2:
apache
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . index.php
8. Consiglio di Percorso
- Inizia con: AdminLTE + Yii2 Basic
- Crea pannello semplice con valori statici
- Aggiungi AJAX per aggiornamenti in tempo reale
- Collega 1 dispositivo reale (es. lampada WiFi)
- Espandi gradualmente
9. Risorse Utili
Vuoi che approfondisca qualche aspetto specifico? Possiamo partire da un esempio concreto di cosa vuoi controllare per primo (luci, temperatura, energia, etc.)?
E se usassi Freeboard ?
Ottima idea! Freeboard è perfetto per il tuo caso. È molto più semplice di sviluppare UI da zero e si integra bene con PHP/Yii2.
Perché Freeboard è ideale:
- Zero sviluppo frontend – Solo configurazione
- Drag & drop dei widget
- Supporto nativo per JSON datasources
- Mobile responsive già pronto
- Open source e self-hostable
Architettura Consigliata:
text
[Dispositivi IoT] → [Yii2 API PHP] → [Freeboard Dashboard]
↑ ↑ ↑
(ESP8266, (Elabora dati, (Visualizza,
Shelly, etc.) salva su DB) controlla)
1. Installazione Freeboard sul tuo ServerPlan
bash
# Nel tuo spazio web (via SSH o file manager) cd public_html # o htdocs git clone https://github.com/Freeboard/freeboard.git domotica-ui cd domotica-ui # Struttura risultante: # public_html/ # domotica-ui/ # Freeboard # domotica-api/ # Yii2 (opzionale in sottocartella)
Configurazione server minima:
- Solo web server (Apache/Nginx)
- Supporto per file statici (HTML/JS/CSS)
- Accesso via HTTP/HTTPS
2. Yii2 come Backend API
Configurazione base API:
php
// config/web.php (Yii2)
'components' => [
'request' => [
'parsers' => [
'application/json' => 'yii\web\JsonParser',
],
'enableCsrfValidation' => false, // Per Freeboard
],
'response' => [
'formatters' => [
'json' => [
'class' => 'yii\web\JsonResponseFormatter',
'prettyPrint' => YII_DEBUG,
],
],
],
],
Endpoint API semplici:
php
// controllers/ApiController.php
namespace app\controllers;
use yii\web\Controller;
use yii\filters\Cors;
class ApiController extends Controller
{
public function behaviors()
{
return [
'cors' => [
'class' => Cors::class,
'cors' => [
'Origin' => ['*'], // Limita al tuo dominio in produzione
'Access-Control-Allow-Credentials' => true,
],
],
];
}
// GET /api/battery-status
public function actionBatteryStatus()
{
\Yii::$app->response->format = \yii\web\Response::FORMAT_JSON;
// Esempio: dati dal database o MQTT
return [
'charge' => 85,
'voltage' => 48.2,
'temperature' => 25.5,
'power' => 3500, // Watt
'timestamp' => date('c'),
'status' => 'charging'
];
}
// GET /api/solar-production
public function actionSolarProduction()
{
return [
'today' => 12.5, // kWh
'current' => 2500, // Watt
'month' => 320
];
}
// POST /api/switch-light
public function actionSwitchLight($id, $state)
{
// Controlla dispositivo reale
$success = $this->controlDevice($id, $state);
return ['success' => $success];
}
}
3. Configurazione Freeboard
dashboard.json di esempio:
json
{
"allow_edit": true,
"plugins": ["datasources"],
"panes": [
{
"width": 1,
"row": {"height": 300},
"col": {"width": 400},
"widgets": [
{
"type": "sparkline",
"title": "Batteria Casa",
"settings": {
"value": "datasources[\"api\"].charge",
"units": "%",
"value_font_size": "40px"
}
}
]
},
{
"width": 1,
"row": {"height": 300},
"col": {"width": 400},
"widgets": [
{
"type": "gauge",
"title": "Carica Batteria",
"settings": {
"value": "datasources[\"api\"].charge",
"min_value": 0,
"max_value": 100,
"units": "%"
}
}
]
},
{
"width": 1,
"widgets": [
{
"type": "indicator",
"title": "Stato Sistema",
"settings": {
"value": "datasources[\"api\"].status",
"on_text": "CARICANDO",
"off_text": "IN ATTESA",
"on_color": "green",
"off_color": "gray"
}
}
]
}
],
"datasources": [
{
"name": "api",
"type": "JSON",
"url": "http://tuodominio.com/domotica-api/api/battery-status",
"refresh": 5000,
"headers": {
"X-API-Key": "tua-chiave-segreta"
}
}
]
}
4. Datasource Plugin Personalizzato per Controlli
Crea plugins/control.js in Freeboard:
javascript
freeboard.addStyle('.switch-button',
"margin:10px; padding:20px; background:#f0f0f0; border-radius:5px;");
freeboard.registerWidget('yii_switch', {
type: "yii_switch",
name: "Yii2 Switch Control",
default_size: { width: 1, height: 1 },
render: function(el, datasource) {
var html = '<button class="btn-on">ACCENDI</button>' +
'<button class="btn-off">SPEGNI</button>';
$(el).html(html);
$(el).find('.btn-on').click(function() {
$.post('http://tuodominio.com/domotica-api/api/switch-light', {
id: this.config.device_id,
state: 1
});
});
},
settings: [
{
name: "device_id",
display_name: "Device ID",
type: "text"
}
]
});
5. Setup Completamente Funzionante
Struttura Directory:
text
/home/tuoutente/public_html/ ├── domotica-ui/ # Freeboard │ ├── index.html │ ├── css/ │ ├── js/ │ └── dashboard.json # Configurazione │ ├── domotica-api/ # Yii2 Application │ ├── web/ │ │ └── index.php # Entry point API │ ├── config/ │ └── controllers/ # ApiController.php │ └── logs/ # Logs comuni
Configurazione Virtual Host (se possibile):
apache
# .htaccess nella root RewriteEngine On # Freeboard UI RewriteRule ^dashboard(/.*)?$ /domotica-ui/index.html [L] # API Yii2 RewriteRule ^api(/.*)?$ /domotica-api/web/index.php [L]
6. Esempi di Widget Pronti
a) Monitoraggio Auto Elettrica:
json
{
"type": "text_widget",
"title": "Auto Elettrica",
"settings": {
"value": "datasources[\"car\"].battery + '% - ' + datasources[\"car\"].range + ' km'",
"font_size": "24px"
}
}
b) Consumi Domestici:
json
{
"type": "chartjs",
"title": "Consumi Oggi",
"settings": {
"chart_type": "bar",
"data": "datasources[\"energy\"].hourly",
"options": {
"responsive": true,
"maintainAspectRatio": false
}
}
}
7. Sicurezza – Passaggi Essenziali
php
// In Yii2 ApiController
public function beforeAction($action)
{
// API Key semplice
$apiKey = Yii::$app->request->headers->get('X-API-Key');
if ($apiKey !== 'tua-chiave-segreta') {
throw new \yii\web\ForbiddenHttpException('Accesso negato');
}
return parent::beforeAction($action);
}
8. Deploy Rapido
Opzione 1: File Manager
- Scarica Freeboard da GitHub
- Carica ZIP sul server e estrai
- Modifica
dashboard.json - Configura URL API
Opzione 2: Comandi SSH
bash
cd ~/public_html wget https://github.com/Freeboard/freeboard/archive/refs/heads/master.zip unzip master.zip mv freeboard-master domotica-ui
9. Vantaggi di Questo Approccio
- Tempo di sviluppo: 1-2 giorni invece di settimane
- Manutenzione: Aggiorni solo le API PHP
- Estensibile: Aggiungi widget quando vuoi
- Mobile: Già responsive
- Open source: Nessun costo licenza
Prossimi Passi Pratici:
- Installa Freeboard sul tuo server (15 minuti)
- Crea 1 endpoint API in Yii2 che restituisce JSON
- Configura 1 widget in Freeboard che legge l’API
- Testa su desktop e mobile
Vuoi che ti mostri come creare il primo endpoint API specifico per i tuoi dispositivi? Possiamo partire da quello che hai già (batterie, auto, etc.) e creare un mini-dashboard in meno di un’ora!
Ottimo, potresti continuare?
Certamente! Continuiamo con l’implementazione pratica. Andiamo passo passo.
PARTE 1: Installazione e Configurazione Rapida
1.1 Installazione Freeboard sul tuo ServerPlan
bash
# Se hai accesso SSH: cd ~/public_html # o ~/htdocs - dipende dal tuo hosting wget https://github.com/Freeboard/freeboard/archive/refs/heads/master.zip unzip master.zip mv freeboard-master domotica-dash cd domotica-dash # Altrimenti, se hai solo file manager: # 1. Vai su https://github.com/Freeboard/freeboard # 2. Download ZIP # 3. Carica e estrai nella cartella pubblic_html/domotica-dash
File essenziali da modificare:
text
domotica-dash/ ├── index.html # Pagina principale └── js/freeboard.js # Eventuali plugin custom
1.2 Configurazione di base Freeboard
Crea config.js nella root:
javascript
// config.js - Carica prima di freeboard.js
window.FREEBOARD_CONFIG = {
// Abilita salvataggio automatico
persistence: {
type: "local", // O "remote" per salvare su tuo server
url: "http://tuodominio.com/api/save-dashboard"
},
// Plugin aggiuntivi
plugins: [
{ name: "chartjs", path: "plugins/chartjs.js" }
],
// Dashboard iniziale
default_dashboard: {
"panes": [
{
"width": 1,
"widgets": [
{
"type": "text",
"title": "Sistema Domotica",
"settings": {
"value": "Benvenuto nel sistema",
"font_size": "24px"
}
}
]
}
]
}
};
PARTE 2: Yii2 API – Backend Completo
2.1 Installazione Yii2 Basic API
bash
# Nella root del tuo server (parallelo a freeboard) cd ~/public_html composer create-project --prefer-dist yiisoft/yii2-app-basic domotica-api cd domotica-api # Installa estensioni utili composer require yiisoft/yii2-redis # Per cache veloce (opzionale) composer require ramsey/uuid # Per ID univoci
2.2 Configurazione Database
config/db.php:
php
<?php
return [
'class' => 'yii\db\Connection',
'dsn' => 'mysql:host=localhost;dbname=tudb_domotica',
'username' => 'tuo_username',
'password' => 'tua_password',
'charset' => 'utf8mb4',
// Cache delle query
'enableSchemaCache' => true,
'schemaCacheDuration' => 3600,
'schemaCache' => 'cache',
];
2.3 Migrazioni Database
migrations/m231214_000001_create_devices_table.php:
php
<?php
use yii\db\Migration;
class m231214_000001_create_devices_table extends Migration
{
public function safeUp()
{
// Dispositivi
$this->createTable('device', [
'id' => $this->primaryKey(),
'name' => $this->string(100)->notNull(),
'type' => $this->string(50)->notNull()->comment('light, switch, sensor, battery'),
'ip_address' => $this->string(15),
'mqtt_topic' => $this->string(100),
'status' => $this->boolean()->defaultValue(false),
'last_value' => $this->decimal(10,2),
'last_update' => $this->timestamp(),
'config' => $this->json(), // Per parametri specifici
]);
// Log dei valori
$this->createTable('device_log', [
'id' => $this->bigPrimaryKey(),
'device_id' => $this->integer()->notNull(),
'value' => $this->decimal(10,2),
'unit' => $this->string(20),
'timestamp' => $this->timestamp()->defaultExpression('CURRENT_TIMESTAMP'),
]);
// Index per performance
$this->createIndex('idx_device_log_device', 'device_log', ['device_id', 'timestamp']);
// Dati di esempio
$this->batchInsert('device',
['name', 'type', 'ip_address', 'status'],
[
['Batteria Casa', 'battery', '192.168.1.100', true],
['Pannello Solare', 'sensor', '192.168.1.101', true],
['Auto Elettrica', 'vehicle', null, true],
['Luce Salotto', 'light', '192.168.1.102', false],
]
);
}
public function safeDown()
{
$this->dropTable('device_log');
$this->dropTable('device');
}
}
Esegui la migrazione:
bash
cd domotica-api ./yii migrate
2.4 Modelli Yii2
models/Device.php:
php
<?php
namespace app\models;
use Yii;
use yii\db\ActiveRecord;
class Device extends ActiveRecord
{
const TYPE_BATTERY = 'battery';
const TYPE_SOLAR = 'solar';
const TYPE_VEHICLE = 'vehicle';
const TYPE_LIGHT = 'light';
public static function tableName()
{
return 'device';
}
public function rules()
{
return [
[['name', 'type'], 'required'],
['type', 'in', 'range' => ['battery', 'solar', 'vehicle', 'light', 'switch', 'sensor']],
['ip_address', 'ip'],
['status', 'boolean'],
['config', 'safe'],
];
}
public function getLogs($limit = 100)
{
return $this->hasMany(DeviceLog::class, ['device_id' => 'id'])
->orderBy(['timestamp' => SORT_DESC])
->limit($limit);
}
public function getLastLog()
{
return $this->hasOne(DeviceLog::class, ['device_id' => 'id'])
->orderBy(['timestamp' => SORT_DESC]);
}
// Metodo per controllare dispositivo
public function sendCommand($command, $value = null)
{
switch($this->type) {
case self::TYPE_LIGHT:
return $this->controlLight($command);
case self::TYPE_BATTERY:
return $this->readBatteryStatus();
// ... altri tipi
}
}
private function controlLight($state)
{
// Esempio con ESP8266 HTTP
$url = "http://{$this->ip_address}/relay";
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, ['state' => $state ? 1 : 0]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 3);
$response = curl_exec($ch);
curl_close($ch);
return $response === 'OK';
}
}
models/DeviceLog.php:
php
<?php
namespace app\models;
use yii\db\ActiveRecord;
class DeviceLog extends ActiveRecord
{
public static function tableName()
{
return 'device_log';
}
public function getDevice()
{
return $this->hasOne(Device::class, ['id' => 'device_id']);
}
}
2.5 Controller API Completo
controllers/ApiController.php:
php
<?php
namespace app\controllers;
use Yii;
use yii\web\Controller;
use yii\filters\Cors;
use yii\filters\ContentNegotiator;
use yii\web\Response;
use app\models\Device;
use app\models\DeviceLog;
class ApiController extends Controller
{
public $enableCsrfValidation = false;
public function behaviors()
{
return [
'cors' => [
'class' => Cors::class,
'cors' => [
'Origin' => ['http://localhost', 'https://tudominio.com'],
'Access-Control-Request-Method' => ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'],
'Access-Control-Request-Headers' => ['*'],
'Access-Control-Allow-Credentials' => null,
'Access-Control-Max-Age' => 86400,
'Access-Control-Expose-Headers' => [],
],
],
'contentNegotiator' => [
'class' => ContentNegotiator::class,
'formats' => [
'application/json' => Response::FORMAT_JSON,
],
],
];
}
/**
* GET /api/dashboard-data
* Tutti i dati per la dashboard principale
*/
public function actionDashboardData()
{
$data = [
'timestamp' => date('c'),
'devices' => [],
'summary' => [],
];
// Batterie
$batteries = Device::find()
->where(['type' => 'battery'])
->with(['lastLog'])
->all();
foreach ($batteries as $battery) {
$data['devices']['batteries'][] = [
'id' => $battery->id,
'name' => $battery->name,
'charge' => $battery->lastLog ? $battery->lastLog->value : 0,
'status' => $battery->status,
'last_update' => $battery->last_update,
];
}
// Produzione solare
$solar = Device::find()
->where(['type' => 'solar'])
->with(['lastLog'])
->one();
if ($solar) {
$data['devices']['solar'] = [
'production' => $solar->lastLog ? $solar->lastLog->value : 0,
'unit' => 'W',
'today' => $this->getTodayProduction($solar->id),
];
}
// Auto elettrica
$vehicle = Device::find()
->where(['type' => 'vehicle'])
->with(['lastLog'])
->one();
if ($vehicle) {
$data['devices']['vehicle'] = [
'battery' => $vehicle->lastLog ? $vehicle->lastLog->value : 0,
'range' => $this->calculateRange($vehicle->lastLog ? $vehicle->lastLog->value : 0),
'charging' => $vehicle->status,
];
}
// Riepilogo energetico
$data['summary'] = $this->calculateEnergySummary();
return $data;
}
/**
* GET /api/device/:id
* Dettaglio dispositivo con storico
*/
public function actionDevice($id, $hours = 24)
{
$device = Device::findOne($id);
if (!$device) {
Yii::$app->response->statusCode = 404;
return ['error' => 'Dispositivo non trovato'];
}
// Storico ultime N ore
$since = date('Y-m-d H:i:s', strtotime("-$hours hours"));
$logs = DeviceLog::find()
->where(['device_id' => $id])
->andWhere(['>=', 'timestamp', $since])
->orderBy(['timestamp' => SORT_ASC])
->all();
return [
'device' => [
'id' => $device->id,
'name' => $device->name,
'type' => $device->type,
'status' => $device->status,
'config' => json_decode($device->config, true) ?: [],
],
'current' => $device->lastLog ? $device->lastLog->value : null,
'history' => array_map(function($log) {
return [
'value' => (float)$log->value,
'timestamp' => $log->timestamp,
];
}, $logs),
];
}
/**
* POST /api/device/:id/control
* Controlla un dispositivo
*/
public function actionControl($id)
{
$device = Device::findOne($id);
if (!$device) {
Yii::$app->response->statusCode = 404;
return ['error' => 'Dispositivo non trovato'];
}
$command = Yii::$app->request->post('command');
$value = Yii::$app->request->post('value');
try {
$success = $device->sendCommand($command, $value);
if ($success) {
// Salva nel log
$log = new DeviceLog();
$log->device_id = $device->id;
$log->value = $value;
$log->unit = 'state';
$log->save();
// Aggiorna ultimo stato
$device->last_value = $value;
$device->last_update = date('Y-m-d H:i:s');
$device->save();
return ['success' => true, 'message' => 'Comando eseguito'];
} else {
return ['success' => false, 'message' => 'Errore nell\'esecuzione'];
}
} catch (\Exception $e) {
Yii::error($e->getMessage(), 'api');
return ['success' => false, 'message' => 'Errore interno'];
}
}
/**
* GET /api/energy/today
* Consumi e produzione di oggi
*/
public function actionEnergyToday()
{
$today = date('Y-m-d');
$production = DeviceLog::find()
->select(['HOUR(timestamp) as hour', 'SUM(value) as total'])
->innerJoinWith(['device'])
->where(['device.type' => 'solar'])
->andWhere(['DATE(timestamp)' => $today])
->groupBy(['HOUR(timestamp)'])
->indexBy('hour')
->all();
$consumption = DeviceLog::find()
->select(['HOUR(timestamp) as hour', 'SUM(value) as total'])
->innerJoinWith(['device'])
->where(['device.type' => 'consumption'])
->andWhere(['DATE(timestamp)' => $today])
->groupBy(['HOUR(timestamp)'])
->indexBy('hour')
->all();
// Formatta per grafico
$hours = range(0, 23);
$chartData = [
'labels' => array_map(function($h) { return sprintf('%02d:00', $h); }, $hours),
'production' => [],
'consumption' => [],
];
foreach ($hours as $hour) {
$chartData['production'][] = isset($production[$hour]) ? (float)$production[$hour]->total : 0;
$chartData['consumption'][] = isset($consumption[$hour]) ? (float)$consumption[$hour]->total : 0;
}
return $chartData;
}
/**
* POST /api/webhook/device-update
* Webhook per aggiornamenti automatici da dispositivi
*/
public function actionWebhookDeviceUpdate()
{
$secret = Yii::$app->request->headers->get('X-Webhook-Secret');
if ($secret !== Yii::$app->params['webhookSecret']) {
Yii::$app->response->statusCode = 403;
return ['error' => 'Accesso non autorizzato'];
}
$data = Yii::$app->request->post();
$device = Device::find()
->where(['ip_address' => $data['ip']])
->orWhere(['mqtt_topic' => $data['topic']])
->one();
if ($device) {
$log = new DeviceLog();
$log->device_id = $device->id;
$log->value = $data['value'];
$log->unit = $data['unit'] ?? 'unknown';
if ($log->save()) {
$device->last_value = $data['value'];
$device->last_update = date('Y-m-d H:i:s');
$device->save();
return ['success' => true];
}
}
return ['success' => false];
}
// Metodi helper privati
private function getTodayProduction($deviceId)
{
$today = date('Y-m-d');
return DeviceLog::find()
->where(['device_id' => $deviceId])
->andWhere(['DATE(timestamp)' => $today])
->sum('value') ?: 0;
}
private function calculateRange($batteryPercent)
{
// Esempio: 500km range completo
return round(($batteryPercent / 100) * 500, 1);
}
private function calculateEnergySummary()
{
$today = date('Y-m-d');
return [
'production_today' => DeviceLog::find()
->innerJoinWith(['device'])
->where(['device.type' => 'solar'])
->andWhere(['DATE(timestamp)' => $today])
->sum('value') ?: 0,
'consumption_today' => DeviceLog::find()
->innerJoinWith(['device'])
->where(['device.type' => 'consumption'])
->andWhere(['DATE(timestamp)' => $today])
->sum('value') ?: 0,
'battery_stored' => Device::find()
->where(['type' => 'battery'])
->sum('last_value') ?: 0,
];
}
}
2.6 Configurazione URL
config/web.php:
php
'components' => [
'urlManager' => [
'enablePrettyUrl' => true,
'showScriptName' => false,
'rules' => [
// API routes
'GET api/dashboard-data' => 'api/dashboard-data',
'GET api/device/<id>' => 'api/device',
'POST api/device/<id>/control' => 'api/control',
'GET api/energy/today' => 'api/energy-today',
'POST api/webhook/device-update' => 'api/webhook-device-update',
// Fallback
'<controller:\w+>/<action:\w+>' => '<controller>/<action>',
],
],
],
PARTE 3: Dashboard Freeboard Personalizzata
3.1 Dashboard JSON Completa
Salva come dashboard.json in Freeboard:
json
{
"version": "1.0",
"allow_edit": true,
"plugins": [
"datasources",
"freeboard/widgets/knob.js",
"freeboard/plugins/chartjs.js"
],
"datasources": [
{
"name": "domotica_api",
"type": "JSON",
"url": "../domotica-api/api/dashboard-data",
"refresh": 10000,
"headers": {
"X-API-Key": "tua-chiave-api"
}
},
{
"name": "energy_chart",
"type": "JSON",
"url": "../domotica-api/api/energy/today",
"refresh": 300000
}
],
"panes": [
{
"id": "pane_energia",
"title": "Energia",
"width": 2,
"col": {"width": 600},
"row": {"height": 400},
"widgets": [
{
"type": "gauge",
"title": "Batteria Casa",
"settings": {
"value": "datasources[\"domotica_api\"].devices.batteries[0].charge",
"min_value": 0,
"max_value": 100,
"units": "%",
"gauge_width": 50,
"donut": true,
"level_colors": ["#ff0000", "#ffff00", "#00ff00"]
}
},
{
"type": "indicator",
"title": "Stato Carica",
"settings": {
"value": "datasources[\"domotica_api\"].devices.batteries[0].status",
"on_text": "CARICANDO",
"off_text": "IN ATTESA",
"on_color": "#00ff00",
"off_color": "#cccccc"
}
}
]
},
{
"id": "pane_solare",
"title": "Pannelli Solari",
"width": 2,
"widgets": [
{
"type": "sparkline",
"title": "Produzione Attuale",
"settings": {
"value": "datasources[\"domotica_api\"].devices.solar.production",
"units": "W",
"value_font_size": "32px",
"refresh": 5000
}
},
{
"type": "text",
"settings": {
"value": "Oggi: " +
"datasources[\"domotica_api\"].devices.solar.today + ' kWh'",
"font_size": "18px"
}
}
]
},
{
"id": "pane_auto",
"title": "Auto Elettrica",
"width": 2,
"widgets": [
{
"type": "gauge",
"title": "Batteria Auto",
"settings": {
"value": "datasources[\"domotica_api\"].devices.vehicle.battery",
"min_value": 0,
"max_value": 100,
"units": "%"
}
},
{
"type": "text",
"title": "Autonomia",
"settings": {
"value": "datasources[\"domotica_api\"].devices.vehicle.range + ' km'",
"font_size": "24px",
"font_weight": "bold"
}
},
{
"type": "indicator",
"title": "Carica",
"settings": {
"value": "datasources[\"domotica_api\"].devices.vehicle.charging",
"on_text": "IN CARICA",
"off_text": "FERMA",
"on_color": "#ff9900",
"off_color": "#666666"
}
}
]
},
{
"id": "pane_grafico",
"title": "Consumi Oggi",
"width": 4,
"height": 2,
"widgets": [
{
"type": "chartjs",
"settings": {
"chart_type": "line",
"data": {
"labels": "datasources[\"energy_chart\"].labels",
"datasets": [
{
"label": "Produzione",
"data": "datasources[\"energy_chart\"].production",
"borderColor": "#4CAF50",
"backgroundColor": "rgba(76, 175, 80, 0.1)"
},
{
"label": "Consumo",
"data": "datasources[\"energy_chart\"].consumption",
"borderColor": "#F44336",
"backgroundColor": "rgba(244, 67, 54, 0.1)"
}
]
},
"options": {
"responsive": true,
"maintainAspectRatio": false,
"scales": {
"y": {
"beginAtZero": true,
"title": {
"display": true,
"text": "Watt"
}
}
}
}
}
}
]
},
{
"id": "pane_controlli",
"title": "Controlli Rapidi",
"width": 2,
"widgets": [
{
"type": "html",
"settings": {
"html": "<div style='padding:10px;'>" +
"<button class='btn-control' data-device='1' data-command='on'>" +
" Accendi Luce Salotto" +
"</button>" +
"<br><br>" +
"<button class='btn-control' data-device='2' data-command='off'>" +
" Spegni Presa Cucina" +
"</button>" +
"</div>"
}
}
]
}
]
}
3.2 JavaScript per Controlli Interattivi
Aggiungi in index.html di Freeboard (dopo freeboard.js):
html
<script>
$(document).ready(function() {
// Inizializza Freeboard con la nostra dashboard
freeboard.initialize(true, function() {
// Dopo il caricamento
setupControls();
setupRealTimeUpdates();
});
function setupControls() {
$(document).on('click', '.btn-control', function() {
var deviceId = $(this).data('device');
var command = $(this).data('command');
$.ajax({
url: '../domotica-api/api/device/' + deviceId + '/control',
method: 'POST',
data: {
command: command,
value: command === 'on' ? 1 : 0
},
headers: {
'X-API-Key': 'tua-chiave-api'
},
success: function(response) {
if (response.success) {
alert('Comando eseguito!');
// Aggiorna i dati
freeboard.loadDatasource('domotica_api');
} else {
alert('Errore: ' + response.message);
}
}
});
});
}
function setupRealTimeUpdates() {
// WebSocket per aggiornamenti in tempo reale (opzionale)
if (typeof WebSocket !== 'undefined') {
var ws = new WebSocket('ws://tuodominio.com:8080');
ws.onmessage = function(event) {
var data = JSON.parse(event.data);
// Aggiorna widget specifici in base al tipo di dato
switch(data.type) {
case 'battery_update':
updateBatteryWidget(data.value);
break;
case 'solar_update':
updateSolarWidget(data.value);
break;
}
};
}
}
// Stili per i bottoni
$('<style>').text(`
.btn-control {
background: linear-gradient(to bottom, #4CAF50, #45a049);
border: none;
color: white;
padding: 12px 24px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 16px;
margin: 4px 2px;
cursor: pointer;
border-radius: 4px;
width: 100%;
transition: background 0.3s;
}
.btn-control:hover {
background: linear-gradient(to bottom, #45a049, #3d8b40);
}
.btn-control:active {
transform: translateY(1px);
}
`).appendTo('head');
});
</script>
PARTE 4: Script di Supporto
4.1 Script per Simulazione Dati (se non hai dispositivi reali)
Crea domotica-api/commands/SimulateDataController.php:
php
<?php
namespace app\commands;
use yii\console\Controller;
use app\models\Device;
use app\models\DeviceLog;
class SimulateDataController extends Controller
{
public function actionRun()
{
echo "Simulazione dati domotica...\n";
while (true) {
$this->updateBattery();
$this->updateSolar();
$this->updateVehicle();
echo "Dati aggiornati: " . date('H:i:s') . "\n";
sleep(10); // Aggiorna ogni 10 secondi
}
}
private function updateBattery()
{
$device = Device::findOne(['type' => 'battery']);
if ($device) {
// Simula carica che oscilla tra 70% e 100%
$charge = 85 + (sin(time() / 100) * 15);
$charge = max(70, min(100, $charge));
$log = new DeviceLog();
$log->device_id = $device->id;
$log->value = round($charge, 1);
$log->unit = '%';
$log->save();
$device->last_value = $charge;
$device->last_update = date('Y-m-d H:i:s');
$device->save();
}
}
private function updateSolar()
{
$device = Device::findOne(['type' => 'solar']);
if ($device) {
// Simula produzione solare (0 di notte, picco a mezzogiorno)
$hour = date('H');
$production = 0;
if ($hour >= 6 && $hour <= 18) {
// Curva a campana per la produzione
$normalized = ($hour - 6) / 12;
$production = 3000 * sin($normalized * pi());
}
// Aggiungi un po' di rumore
$production += rand(-100, 100);
$production = max(0, $production);
$log = new DeviceLog();
$log->device_id = $device->id;
$log->value = round($production);
$log->unit = 'W';
$log->save();
$device->last_value = $production;
$device->last_update = date('Y-m-d H:i:s');
$device->save();
}
}
}
Esegui (via SSH o cron):
bash
cd domotica-api ./yii simulate-data/run > /dev/null 2>&1 &
4.2 Cron Job per Pulizia e Report
Crea domotica-api/commands/CronController.php:
php
<?php
namespace app\commands;
use yii\console\Controller;
use yii\db\Expression;
class CronController extends Controller
{
// Esegui ogni giorno a mezzanotte
public function actionDaily()
{
// Pulisci log vecchi di 30 giorni
$thirtyDaysAgo = date('Y-m-d H:i:s', strtotime('-30 days'));
\Yii::$app->db->createCommand()
->delete('device_log', ['<', 'timestamp', $thirtyDaysAgo])
->execute();
echo "Log vecchi puliti\n";
// Crea report giornaliero
$this->createDailyReport();
}
private function createDailyReport()
{
$yesterday = date('Y-m-d', strtotime('-1 day'));
$report = [
'date' => $yesterday,
'total_production' => 0,
'total_consumption' => 0,
'devices' => [],
];
// Salva su file o database
$filename = \Yii::getAlias('@app/runtime/reports/') . $yesterday . '.json';
file_put_contents($filename, json_encode($report, JSON_PRETTY_PRINT));
echo "Report creato: $filename\n";
}
}
Configura cron su ServerPlan:
bash
# Aggiungi al crontab (crontab -e) 0 0 * * * cd /home/tuoutente/public_html/domotica-api && /usr/bin/php yii cron/daily */5 * * * * cd /home/tuoutente/public_html/domotica-api && /usr/bin/php yii simulate-data/run --oneshot
PARTE 5: Sicurezza Avanzata
5.1 Middleware di Autenticazione
Crea components/ApiAuth.php:
php
<?php
namespace app\components;
use Yii;
use yii\filters\auth\HttpBearerAuth;
use yii\filters\auth\QueryParamAuth;
class ApiAuth extends HttpBearerAuth
{
public function authenticate($user, $request, $response)
{
// API Key in header o query string
$apiKey = $request->headers->get('X-API-Key')
?: $request->get('api_key');
if ($apiKey && $this->validateApiKey($apiKey)) {
// Crea utente fittizio per Yii2
$identity = new \yii\web\User();
$identity->id = 'api_user';
$user->switchIdentity($identity);
return $identity;
}
return null;
}
private function validateApiKey($key)
{
// Verifica contro lista di chiavi valide
$validKeys = Yii::$app->params['apiKeys'] ?? [];
return in_array($key, $validKeys);
}
}
5.2 Rate Limiting
Aggiungi in ApiController:
php
use yii\filters\RateLimiter;
public function behaviors()
{
$behaviors = parent::behaviors();
$behaviors['rateLimiter'] = [
'class' => RateLimiter::class,
'enableRateLimitHeaders' => true,
'errorMessage' => 'Troppe richieste',
];
return $behaviors;
}
PARTE 6: Deploy e Test
6.1 Controllo Finale
Struttura completa:
text
public_html/
├── domotica-dash/ # Freeboard UI
│ ├── index.html
│ ├── dashboard.json
│ ├── js/
│ └── css/
│
├── domotica-api/ # Yii2 Backend
│ ├── web/
│ │ └── index.php
│ ├── controllers/
│ │ └── ApiController.php
│ ├── models/
│ ├── migrations/
│ └── config/
│
└── domotica-data/ # Dati persistenti (opzionale)
├── logs/
└── reports/
6.2 Test Endpoint API
bash
# Test API con curl
curl -H "X-API-Key: tua-chiave" \
https://tudominio.com/domotica-api/api/dashboard-data
# Risposta attesa:
{
"timestamp": "2023-12-14T10:30:00+01:00",
"devices": {
"batteries": [
{
"id": 1,
"name": "Batteria Casa",
"charge": 85.5,
"status": true
}
],
"solar": {
"production": 2450,
"unit": "W",
"today": 12.3
}
}
}
6.3 Accesso alla Dashboard
URL della dashboard:
text
https://tudominio.com/domotica-dash/
URL API:
text
https://tudominio.com/dom
ottimo grazie. Osservo che è molto articolato e senza il tuoi aiuto qualsiasi modifica sarebbe impossibile…
Hai ragione, è diventato troppo complesso! Semplifichiamo radicalmente. Ti propongo un approccio molto più semplice che puoi gestire da solo.
VERSIONE SUPER SEMPLICE – 3 File Totali!
1. Struttura Minimalista
text
public_html/ ├── dashboard.php # UI + Logica tutto insieme ├── config.php # Configurazione └── dati.json # Dati dei dispositivi (file JSON)
2. File 1: config.php
php
<?php
// CONFIGURAZIONE SEMPLICE
define('PASSWORD', 'domotica123'); // Cambia questa!
define('DATA_FILE', __DIR__ . '/dati.json');
// Dispositivi preconfigurati (puoi modificare)
$dispositivi = [
'batteria_casa' => [
'nome' => 'Batteria Casa',
'tipo' => 'batteria',
'valore' => 85,
'unita' => '%',
'colore' => 'success'
],
'pannelli_solari' => [
'nome' => 'Pannelli Solari',
'tipo' => 'produzione',
'valore' => 2500,
'unita' => 'W',
'colore' => 'warning'
],
'auto_elettrica' => [
'nome' => 'Auto Elettrica',
'tipo' => 'veicolo',
'valore' => 72,
'unita' => '%',
'colore' => 'info'
],
'luce_salotto' => [
'nome' => 'Luce Salotto',
'tipo' => 'interruttore',
'valore' => 0, // 0=spento, 1=acceso
'unita' => '',
'colore' => 'secondary'
]
];
?>
3. File 2: dashboard.php (TUTTO IN UNO!)
php
<?php
include 'config.php';
// Controlla password semplice
session_start();
if (!isset($_SESSION['loggato'])) {
if ($_POST['password'] === PASSWORD) {
$_SESSION['loggato'] = true;
} elseif (isset($_POST['password'])) {
$errore = "Password errata!";
}
}
if (!isset($_SESSION['loggato'])) {
// MOSTRA PAGINA DI LOGIN
?>
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Accesso Domotica</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
body { background: #f5f5f5; padding-top: 100px; }
.login-box { max-width: 400px; margin: auto; background: white; padding: 30px; border-radius: 10px; }
</style>
</head>
<body>
<div class="login-box">
<h3 class="text-center mb-4">🔐 Sistema Domotica</h3>
<?php if(isset($errore)) echo "<div class='alert alert-danger'>$errore</div>"; ?>
<form method="post">
<div class="mb-3">
<input type="password" name="password" class="form-control" placeholder="Inserisci password" required>
</div>
<button type="submit" class="btn btn-primary w-100">Accedi</button>
</form>
</div>
</body>
</html>
<?php exit;
}
// LEGGI DATI ESISTENTI
if (file_exists(DATA_FILE)) {
$dati = json_decode(file_get_contents(DATA_FILE), true);
} else {
$dati = $dispositivi; // Usa quelli predefiniti
}
// GESTISCI COMANDI
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (isset($_POST['aggiorna'])) {
// Aggiorna valori casuali (simulazione)
foreach ($dati as $key => &$disp) {
if ($disp['tipo'] === 'batteria') {
$disp['valore'] = max(0, min(100, $disp['valore'] + rand(-2, 2)));
} elseif ($disp['tipo'] === 'produzione') {
// Produzione solare: alta di giorno, bassa di notte
$ora = date('H');
if ($ora >= 6 && $ora <= 18) {
$disp['valore'] = rand(2000, 3500);
} else {
$disp['valore'] = rand(0, 100);
}
}
}
// Salva su file
file_put_contents(DATA_FILE, json_encode($dati, JSON_PRETTY_PRINT));
}
if (isset($_POST['interruttore'])) {
$device = $_POST['device'];
$dati[$device]['valore'] = $dati[$device]['valore'] ? 0 : 1;
file_put_contents(DATA_FILE, json_encode($dati, JSON_PRETTY_PRINT));
}
}
// CALCOLA STATISTICHE
$totale_batteria = 0;
$conta_batteria = 0;
$produzione_attuale = 0;
foreach ($dati as $disp) {
if ($disp['tipo'] === 'batteria') {
$totale_batteria += $disp['valore'];
$conta_batteria++;
}
if ($disp['tipo'] === 'produzione') {
$produzione_attuale = $disp['valore'];
}
}
$media_batteria = $conta_batteria > 0 ? round($totale_batteria / $conta_batteria, 1) : 0;
?>
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dashboard Domotica Semplice</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.8.1/font/bootstrap-icons.css">
<style>
body { background: #f8f9fa; }
.card {
border-radius: 15px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
margin-bottom: 20px;
transition: transform 0.3s;
}
.card:hover { transform: translateY(-5px); }
.progress { height: 25px; border-radius: 10px; }
.btn-action { width: 100%; padding: 10px; }
.stat-box {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 20px;
border-radius: 10px;
margin-bottom: 20px;
}
.device-icon { font-size: 2rem; margin-bottom: 10px; }
</style>
</head>
<body>
<!-- NAVBAR -->
<nav class="navbar navbar-dark bg-dark">
<div class="container-fluid">
<span class="navbar-brand">
<i class="bi bi-house"></i> Domotica Casa
</span>
<form method="post" class="d-flex">
<button name="aggiorna" class="btn btn-outline-light me-2">
<i class="bi bi-arrow-clockwise"></i> Aggiorna
</button>
<a href="?logout" class="btn btn-danger">Esci</a>
</form>
</div>
</nav>
<div class="container mt-4">
<!-- STATISTICHE -->
<div class="row">
<div class="col-md-4">
<div class="stat-box text-center">
<i class="bi bi-battery-charging device-icon"></i>
<h3><?= $media_batteria ?>%</h3>
<p>Batteria Media</p>
</div>
</div>
<div class="col-md-4">
<div class="stat-box text-center" style="background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);">
<i class="bi bi-sun device-icon"></i>
<h3><?= $produzione_attuale ?>W</h3>
<p>Produzione Solare</p>
</div>
</div>
<div class="col-md-4">
<div class="stat-box text-center" style="background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);">
<i class="bi bi-car-front device-icon"></i>
<h3><?= $dati['auto_elettrica']['valore'] ?>%</h3>
<p>Auto Elettrica</p>
</div>
</div>
</div>
<!-- DISPOSITIVI -->
<div class="row">
<?php foreach ($dati as $key => $dispositivo): ?>
<div class="col-md-3 col-sm-6">
<div class="card">
<div class="card-body text-center">
<!-- Icona -->
<?php
$icone = [
'batteria' => 'bi-battery-charging',
'produzione' => 'bi-sun',
'veicolo' => 'bi-car-front',
'interruttore' => 'bi-lightbulb'
];
$icona = $icone[$dispositivo['tipo']] ?? 'bi-device-hdd';
?>
<i class="bi <?= $icona ?> text-<?= $dispositivo['colore'] ?>" style="font-size: 2.5rem;"></i>
<h5 class="card-title mt-2"><?= $dispositivo['nome'] ?></h5>
<!-- Visualizzazione Valore -->
<?php if ($dispositivo['tipo'] === 'batteria' || $dispositivo['tipo'] === 'veicolo'): ?>
<div class="progress mt-3">
<div class="progress-bar bg-<?= $dispositivo['colore'] ?>"
role="progressbar"
style="width: <?= $dispositivo['valore'] ?>%">
<?= $dispositivo['valore'] ?>%
</div>
</div>
<?php elseif ($dispositivo['tipo'] === 'produzione'): ?>
<h2 class="text-<?= $dispositivo['colore'] ?>">
<?= number_format($dispositivo['valore']) ?>
<small class="fs-6"><?= $dispositivo['unita'] ?></small>
</h2>
<?php elseif ($dispositivo['tipo'] === 'interruttore'): ?>
<form method="post" class="mt-3">
<input type="hidden" name="device" value="<?= $key ?>">
<button name="interruttore"
class="btn btn-<?= $dispositivo['valore'] ? 'success' : 'secondary' ?> btn-action">
<i class="bi bi-power"></i>
<?= $dispositivo['valore'] ? 'ACCESO' : 'SPENTO' ?>
</button>
</form>
<?php endif; ?>
<!-- Timestamp -->
<small class="text-muted d-block mt-2">
<i class="bi bi-clock"></i> Ultimo aggiornamento: <?= date('H:i') ?>
</small>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
<!-- PANNELLO CONTROLLI -->
<div class="row mt-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5><i class="bi bi-sliders"></i> Controlli Rapidi</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-3">
<button class="btn btn-outline-primary w-100 mb-2">
<i class="bi bi-sunrise"></i> Modalità Giorno
</button>
</div>
<div class="col-md-3">
<button class="btn btn-outline-secondary w-100 mb-2">
<i class="bi bi-moon"></i> Modalità Notte
</button>
</div>
<div class="col-md-3">
<button class="btn btn-outline-success w-100 mb-2">
<i class="bi bi-lightning-charge"></i> Risparmio Energetico
</button>
</div>
<div class="col-md-3">
<button class="btn btn-outline-danger w-100 mb-2">
<i class="bi bi-shield-exclamation"></i> Tutto Spento
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- LOG ATTIVITÀ -->
<div class="row mt-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5><i class="bi bi-list-check"></i> Ultime Attività</h5>
</div>
<div class="card-body">
<ul class="list-group">
<li class="list-group-item">
<i class="bi bi-check-circle text-success"></i>
Sistema aggiornato alle <?= date('H:i:s') ?>
</li>
<li class="list-group-item">
<i class="bi bi-battery text-info"></i>
Batteria casa: <?= $dati['batteria_casa']['valore'] ?>%
</li>
<li class="list-group-item">
<i class="bi bi-sun text-warning"></i>
Produzione solare: <?= $dati['pannelli_solari']['valore'] ?>W
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<!-- JavaScript per aggiornamento automatico -->
<script>
// Aggiorna la pagina ogni 30 secondi
setTimeout(function() {
document.querySelector('[name="aggiorna"]').click();
}, 30000);
// Animazioni semplici
document.querySelectorAll('.card').forEach(card => {
card.addEventListener('click', function() {
this.style.transform = 'scale(0.98)';
setTimeout(() => {
this.style.transform = '';
}, 200);
});
});
</script>
</body>
</html>
<?php
// Logout
if (isset($_GET['logout'])) {
session_destroy();
header('Location: dashboard.php');
exit;
}
?>
4. File 3: .htaccess (per sicurezza)
apache
# Blocca accesso diretto al file dati
<Files "dati.json">
Order Allow,Deny
Deny from all
</Files>
# Forza HTTPS se disponibile
RewriteEngine On
RewriteCond %{HTTPS} off
RewriteRule ^(.*)$ https://%{HTTP_HOST}/$1 [R=301,L]
COME USARE:
- Carica i 3 file sul tuo ServerPlan
- Modifica
config.php:- Cambia la password
domotica123 - Modifica i dispositivi come vuoi
- Cambia la password
- Visita
https://tuodominio.com/dashboard.php - Login con la password che hai scelto
FUNZIONALITÀ PRONTE:
✅ Dashboard responsive (funziona su telefono)
✅ Password di accesso
✅ Interruttori ON/OFF
✅ Grafici a barre per batterie
✅ Aggiornamento automatico ogni 30 secondi
✅ Statistiche in tempo reale
✅ Nessun database – tutto su file JSON
✅ Design moderno con Bootstrap 5
COME MODIFICARE:
Aggiungere un nuovo dispositivo:
php
// In config.php, aggiungi nell'array $dispositivi:
'termosifone' => [
'nome' => 'Termosifone Sala',
'tipo' => 'interruttore', // oppure 'termostato'
'valore' => 0,
'unita' => '',
'colore' => 'primary'
]
Cambiare colori:
Usa i colori Bootstrap: primary, success, warning, danger, info, secondary
Cambiare l’icona:
Vedi https://icons.getbootstrap.com/ – usa bi-nome-icona
PROSSIMI PASSI (quando ti senti pronto):
- Aggiungi dispositivi reali – modificando le funzioni di controllo
- Aggiungi grafici – con Chart.js (1 file in più)
- Notifiche – con Telegram o email
- Cron job – per salvare dati giornalieri
VANTAGGI DI QUESTA VERSIONE:
🚀 Installazione: 5 minuti
🔧 Manutenzione: Modifichi 1 file e capisci tutto
📱 Mobile: Già responsive
🔒 Sicuro: Con password
💾 Nessun database: Più semplice da gestire
Questo lo puoi gestire completamente da solo! Parti da qui e quando ti senti sicuro, espandi gradualmente.
Vuoi che ti mostri come aggiungere il primo dispositivo reale (es. una presa WiFi)?
si grazie
Perfetto! Aggiungiamo dispositivi reali passo dopo passo. Cominciamo con i dispositivi più comuni e semplici.
1. PRESA/SWITCH WiFi (TP-Link Kasa, Sonoff, Shelly)
Configurazione per Shelly (il più semplice):
Prima modifica config.php:
php
// Aggiungi dopo l'array $dispositivi
$dispositivi_reali = [
'shelly_salotto' => [
'nome' => 'Luce Salotto',
'tipo' => 'interruttore',
'ip' => '192.168.1.100', // IP del tuo Shelly
'model' => 'shelly1',
'valore' => 0
],
'tplink_cucina' => [
'nome' => 'Presa Cucina',
'tipo' => 'interruttore',
'ip' => '192.168.1.101',
'model' => 'tplink',
'username' => 'admin', // Se richiesto
'password' => 'password' // Se richiesto
]
];
File aggiuntivo: dispositivi_reali.php
php
<?php
// FUNZIONI PER CONTROLLARE DISPOSITIVI REALI
class DispositiviReali {
// CONTROLLA SHELLY (HTTP semplice)
public static function controllaShelly($ip, $relay = 0, $state = null) {
if ($state !== null) {
// Imposta stato
$url = "http://$ip/relay/$relay?turn=" . ($state ? "on" : "off");
$response = file_get_contents($url);
return strpos($response, '"ison":true') !== false;
} else {
// Leggi stato
$url = "http://$ip/status";
$response = file_get_contents($url);
$data = json_decode($response, true);
return $data['relays'][$relay]['ison'] ?? false;
}
}
// CONTROLLA TP-LINK KASA
public static function controllaTPLink($ip, $state = null) {
// TP-Link usa protocollo proprietario, ma c'è una libreria PHP
require_once 'tplink.class.php'; // Scarica da GitHub
$plug = new TPLinkSmartPlug($ip);
if ($state !== null) {
return $state ? $plug->powerOn() : $plug->powerOff();
} else {
return $plug->getRelayState();
}
}
// CONTROLLA SONOFF BASIC (Tasmota)
public static function controllaSonoff($ip, $state = null) {
if ($state !== null) {
$url = "http://$ip/cm?cmnd=Power%20" . ($state ? "On" : "Off");
$response = file_get_contents($url);
$data = json_decode($response, true);
return $data['POWER'] === "ON";
} else {
$url = "http://$ip/cm?cmnd=status";
$response = file_get_contents($url);
$data = json_decode($response, true);
return $data['Status']['Power'] === 1;
}
}
// LEGGI SENSORE DI TEMPERATURA SHELLY
public static function leggiTemperaturaShelly($ip) {
$url = "http://$ip/status";
$response = file_get_contents($url);
$data = json_decode($response, true);
if (isset($data['tmp']['tC'])) {
return $data['tmp']['tC']; // Temperatura in Celsius
} elseif (isset($data['temperature'])) {
return $data['temperature'];
}
return null;
}
// LEGGI CONSUMO ENERGETICO (Shelly EM/3EM)
public static function leggiConsumoShelly($ip) {
$url = "http://$ip/status";
$response = @file_get_contents($url);
if ($response === false) return null;
$data = json_decode($response, true);
// Shelly EM misura consumo
if (isset($data['emeters'][0]['power'])) {
return $data['emeters'][0]['power']; // Watt
}
return null;
}
}
?>
Integrazione nella dashboard:
Modifica dashboard.php – aggiungi all’inizio:
php
// ... dopo include 'config.php'
include 'dispositivi_reali.php';
// LEGGI STATO DISPOSITIVI REALI
if (file_exists(DATA_FILE)) {
$dati = json_decode(file_get_contents(DATA_FILE), true);
// Aggiorna valori da dispositivi reali
foreach ($dispositivi_reali as $key => $device) {
if (isset($dati[$key])) {
try {
switch($device['model']) {
case 'shelly1':
$stato = DispositiviReali::controllaShelly($device['ip']);
$dati[$key]['valore'] = $stato ? 1 : 0;
// Se ha sensore temperatura
$temp = DispositiviReali::leggiTemperaturaShelly($device['ip']);
if ($temp !== null) {
$dati[$key]['temperatura'] = $temp;
}
break;
case 'tplink':
$stato = DispositiviReali::controllaTPLink($device['ip']);
$dati[$key]['valore'] = $stato ? 1 : 0;
break;
}
} catch (Exception $e) {
// Dispositivo non raggiungibile
$dati[$key]['offline'] = true;
}
}
}
} else {
$dati = array_merge($dispositivi, $dispositivi_reali);
}
Modifica la sezione interruttori nella dashboard:
php
<?php elseif ($dispositivo['tipo'] === 'interruttore'): ?>
<?php
$offline = isset($dispositivo['offline']) && $dispositivo['offline'];
$stato_reale = $dispositivo['valore'];
?>
<div class="mb-3">
<?php if($offline): ?>
<div class="alert alert-warning">
<i class="bi bi-wifi-off"></i> Dispositivo offline
</div>
<?php endif; ?>
<form method="post" class="d-flex gap-2">
<input type="hidden" name="device" value="<?= $key ?>">
<button name="accendi"
class="btn btn-<?= $stato_reale ? 'success' : 'outline-success' ?> flex-fill"
<?= $offline ? 'disabled' : '' ?>>
<i class="bi bi-power"></i> ON
</button>
<button name="spegni"
class="btn btn-<?= !$stato_reale ? 'danger' : 'outline-danger' ?> flex-fill"
<?= $offline ? 'disabled' : '' ?>>
<i class="bi bi-power"></i> OFF
</button>
</form>
<?php if(isset($dispositivo['temperatura'])): ?>
<div class="mt-2 text-center">
<small>
<i class="bi bi-thermometer"></i>
<?= $dispositivo['temperatura'] ?>°C
</small>
</div>
<?php endif; ?>
</div>
<?php endif; ?>
Gestisci i comandi in cima al file:
php
// GESTISCI COMANDI PER DISPOSITIVI REALI
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// ... codice esistente ...
if (isset($_POST['accendi']) || isset($_POST['spegni'])) {
$device_key = $_POST['device'];
$state = isset($_POST['accendi']) ? 1 : 0;
if (isset($dispositivi_reali[$device_key])) {
$device = $dispositivi_reali[$device_key];
try {
switch($device['model']) {
case 'shelly1':
$success = DispositiviReali::controllaShelly($device['ip'], 0, $state);
break;
case 'tplink':
$success = DispositiviReali::controllaTPLink($device['ip'], $state);
break;
case 'sonoff':
$success = DispositiviReali::controllaSonoff($device['ip'], $state);
break;
default:
$success = false;
}
if ($success) {
$dati[$device_key]['valore'] = $state;
$dati[$device_key]['ultimo_comando'] = date('H:i:s');
$_SESSION['messaggio'] = "Comando eseguito!";
}
} catch (Exception $e) {
$_SESSION['errore'] = "Errore: " . $e->getMessage();
}
file_put_contents(DATA_FILE, json_encode($dati, JSON_PRETTY_PRINT));
}
}
}
// Mostra messaggi
if (isset($_SESSION['messaggio'])) {
echo '<div class="alert alert-success alert-dismissible fade show">
' . $_SESSION['messaggio'] . '
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>';
unset($_SESSION['messaggio']);
}
2. SENSORI TEMPERATURA/UMIDITÀ (DHT22, BME280)
Configurazione per ESP8266/ESP32 con web server:
Il microcontrollore ESP deve esporre un endpoint HTTP:
c++
// Codice Arduino/ESP8266 semplificato
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
ESP8266WebServer server(80);
void setup() {
// Connessione WiFi
WiFi.begin("SSID", "password");
// Endpoint API
server.on("/api/temperature", []() {
float temp = readTemperature(); // Tua funzione
float humidity = readHumidity();
String json = "{";
json += "\"temperature\":" + String(temp, 1);
json += ",\"humidity\":" + String(humidity, 1);
json += ",\"device\":\"sensor_camera\"";
json += "}";
server.send(200, "application/json", json);
});
server.begin();
}
Aggiungi al tuo PHP:
In dispositivi_reali.php:
php
// LEGGI SENSORE ESP8266/ESP32
public static function leggiSensoreESP($ip) {
$url = "http://$ip/api/temperature";
$context = stream_context_create([
'http' => ['timeout' => 3] // 3 secondi timeout
]);
$response = @file_get_contents($url, false, $context);
if ($response === false) return null;
return json_decode($response, true);
}
// LEGGI MULTIPLI SENSORI (array di IP)
public static function leggiTuttiSensori($sensori) {
$results = [];
foreach ($sensori as $nome => $ip) {
$data = self::leggiSensoreESP($ip);
if ($data) {
$results[$nome] = $data;
}
}
return $results;
}
Configura i sensori in config.php:
php
$sensori = [
'camera' => [
'nome' => 'Temperatura Camera',
'ip' => '192.168.1.110',
'tipo' => 'sensore',
'unita_temp' => '°C',
'unita_umid' => '%'
],
'esterno' => [
'nome' => 'Temperatura Esterna',
'ip' => '192.168.1.111',
'tipo' => 'sensore'
]
];
Visualizzazione nella dashboard:
php
<!-- Aggiungi dopo la sezione dispositivi -->
<div class="row mt-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5><i class="bi bi-thermometer-half"></i> Sensori Ambientali</h5>
</div>
<div class="card-body">
<div class="row">
<?php
$dati_sensori = DispositiviReali::leggiTuttiSensori($sensori);
foreach ($dati_sensori as $nome => $sensore):
?>
<div class="col-md-4">
<div class="card">
<div class="card-body text-center">
<h6><?= $sensori[$nome]['nome'] ?></h6>
<div class="display-4">
<?= $sensore['temperature'] ?? '--' ?>
<small class="fs-6">°C</small>
</div>
<div class="text-muted">
<i class="bi bi-droplet"></i>
Umidità: <?= $sensore['humidity'] ?? '--' ?>%
</div>
<small class="text-muted">
<i class="bi bi-clock"></i> <?= date('H:i') ?>
</small>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
</div>
</div>
</div>
3. CONTROLLO AUTOMOBILI (API Tesla, altre auto)
Per Tesla (API ufficiale):
Crea tesla_api.php:
php
<?php
class TeslaAPI {
private $email;
private $password;
private $access_token;
public function __construct($email, $password) {
$this->email = $email;
$this->password = $password;
$this->login();
}
private function login() {
$data = [
'grant_type' => 'password',
'client_id' => 'ownerapi',
'client_secret' => 'c7257eb71a564034f9419ee651c7d0e5f7aa6bfbd18bafb5c5c033b093bb2fa3',
'email' => $this->email,
'password' => $this->password
];
$response = $this->request('https://auth.tesla.com/oauth2/v3/token', $data);
$this->access_token = $response['access_token'];
}
public function getVehicleData() {
// Prima ottieni l'ID veicolo
$vehicles = $this->request('https://owner-api.teslamotors.com/api/1/vehicles');
if (empty($vehicles['response'])) {
return null;
}
$vehicle_id = $vehicles['response'][0]['id_s'];
$data = $this->request("https://owner-api.teslamotors.com/api/1/vehicles/$vehicle_id/vehicle_data");
return [
'battery' => $data['response']['charge_state']['battery_level'] ?? 0,
'range' => $data['response']['charge_state']['battery_range'] ?? 0,
'charging' => $data['response']['charge_state']['charging_state'] === 'Charging',
'temperature' => $data['response']['climate_state']['inside_temp'] ?? 0
];
}
public function startCharging() {
// Implementa avvio carica
}
public function setChargeLimit($percent) {
// Imposta limite carica
}
private function request($url, $data = null) {
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
if ($data) {
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
}
if ($this->access_token) {
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Bearer ' . $this->access_token,
'Content-Type: application/json'
]);
}
$response = curl_exec($ch);
curl_close($ch);
return json_decode($response, true);
}
}
?>
Integrazione nella dashboard:
php
// In config.php
$auto_elettrica = [
'tesla' => [
'nome' => 'Tesla Model 3',
'tipo' => 'veicolo',
'marca' => 'tesla',
'email' => 'tua@email.com',
'password' => 'tua_password'
]
];
// In dashboard.php
try {
require_once 'tesla_api.php';
$tesla = new TeslaAPI(
$auto_elettrica['tesla']['email'],
$auto_elettrica['tesla']['password']
);
$vehicle_data = $tesla->getVehicleData();
if ($vehicle_data) {
$dati['auto_elettrica']['valore'] = $vehicle_data['battery'];
$dati['auto_elettrica']['range'] = $vehicle_data['range'];
$dati['auto_elettrica']['caricando'] = $vehicle_data['charging'];
}
} catch (Exception $e) {
// Gestisci errore
}
4. BATTERIE DOMESTICHE (Powerwall, altre)
Per Powerwall Tesla:
php
class PowerwallAPI {
private $ip;
public function __construct($ip) {
$this->ip = $ip;
}
public function getStatus() {
// Powerwall API locale
$url = "https://{$this->ip}/api/system_status/soe";
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // Per self-signed cert
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
// Se richiede autenticazione
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_setopt($ch, CURLOPT_USERPWD, "customer:password");
$response = curl_exec($ch);
curl_close($ch);
$data = json_decode($response, true);
return [
'charge' => $data['percentage'] ?? 0,
'power' => $data['instant_power'] ?? 0, // Watt
'grid_power' => $this->getGridPower()
];
}
private function getGridPower() {
$url = "https://{$this->ip}/api/meters/aggregates";
// ... richiesta simile
}
}
5. AUTOMAZIONI SEMPLICI
Crea automazioni.php:
php
<?php
class Automazioni {
// ACCENDI LUCI AL TRAMONTO
public static function controlloIlluminazione() {
$ora_attuale = date('H');
$alba = 7; // 7:00
$tramonto = 19; // 19:00
// Se è notte e le luci sono spente
if ($ora_attuale >= $tramonto || $ora_attuale < $alba) {
$luci = ['shelly_salotto', 'shelly_cucina'];
foreach ($luci as $luce) {
// Controlla se qualcuno è in casa (presenza)
if (self::rilevaPresenza()) {
DispositiviReali::controllaShelly($luce, 0, true);
}
}
}
}
// CONTROLLO TEMPERATURA AUTOMATICO
public static function controlloTermostato($temperatura_attuale, $soglia = 21) {
if ($temperatura_attuale < $soglia - 2) {
// Accendi riscaldamento
DispositiviReali::controllaShelly('termostato', 0, true);
} elseif ($temperatura_attuale > $soglia + 2) {
// Spegni riscaldamento
DispositiviReali::controllaShelly('termostato', 0, false);
}
}
// RILEVA PRESENZA (simulata - poi implementa con sensori)
private static function rilevaPresenza() {
// Per ora: sempre vero dalle 18 alle 22
$ora = date('H');
return ($ora >= 18 && $ora <= 22);
}
// MODALITÀ "VIA"
public static function modalitaVia() {
$dispositivi_da_spegnere = [
'shelly_salotto',
'shelly_cucina',
'tplink_camera'
];
foreach ($dispositivi_da_spegnere as $device) {
DispositiviReali::controllaShelly($device, 0, false);
}
// Mantieni accesa solo la sicurezza
DispositiviReali::controllaShelly('allarme', 0, true);
}
}
?>
Integrazione nella dashboard:
php
<!-- Pulsanti automazioni -->
<div class="row mt-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5><i class="bi bi-robot"></i> Automazioni</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-3">
<form method="post">
<button name="automazione" value="modalita_casa"
class="btn btn-outline-primary w-100">
<i class="bi bi-house-door"></i> Modalità Casa
</button>
</form>
</div>
<div class="col-md-3">
<form method="post">
<button name="automazione" value="modalita_via"
class="btn btn-outline-secondary w-100">
<i class="bi bi-car-front"></i> Modalità Via
</button>
</form>
</div>
<div class="col-md-3">
<form method="post">
<button name="automazione" value="tutto_spento"
class="btn btn-outline-danger w-100">
<i class="bi bi-power"></i> Tutto Spento
</button>
</form>
</div>
<div class="col-md-3">
<form method="post">
<button name="automazione" value="risparmio_energetico"
class="btn btn-outline-success w-100">
<i class="bi bi-lightning-charge"></i> Risparmio
</button>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Gestisci comandi -->
<?php
if (isset($_POST['automazione'])) {
require_once 'automazioni.php';
switch($_POST['automazione']) {
case 'modalita_via':
Automazioni::modalitaVia();
$_SESSION['messaggio'] = "Modalità 'VIA' attivata";
break;
case 'tutto_spento':
// Spegni tutto
foreach ($dispositivi_reali as $key => $device) {
if ($device['tipo'] === 'interruttore') {
DispositiviReali::controllaShelly($device['ip'], 0, false);
}
}
$_SESSION['messaggio'] = "Tutto spento";
break;
}
}
?>
6. SCHEDA RIASSUNTIVA PER DISPOSITIVI REALI
Crea una tabella di stato nella dashboard:
php
<!-- Tabella stato dispositivi -->
<div class="card mt-4">
<div class="card-header">
<h5><i class="bi bi-list-check"></i> Stato Dispositivi</h5>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>Dispositivo</th>
<th>Tipo</th>
<th>Stato</th>
<th>Valore</th>
<th>Ultimo Aggiornamento</th>
<th>Azioni</th>
</tr>
</thead>
<tbody>
<?php foreach ($dispositivi_reali as $key => $device):
$stato = $dati[$key]['valore'] ?? 0;
$offline = isset($dati[$key]['offline']) && $dati[$key]['offline'];
?>
<tr class="<?= $offline ? 'table-warning' : '' ?>">
<td>
<i class="bi bi-<?= $stato ? 'check-circle text-success' : 'x-circle text-secondary' ?>"></i>
<?= $device['nome'] ?>
</td>
<td><?= $device['model'] ?? 'N/A' ?></td>
<td>
<?php if($offline): ?>
<span class="badge bg-warning">Offline</span>
<?php else: ?>
<span class="badge bg-<?= $stato ? 'success' : 'secondary' ?>">
<?= $stato ? 'ON' : 'OFF' ?>
</span>
<?php endif; ?>
</td>
<td>
<?php if(isset($dati[$key]['temperatura'])): ?>
<?= $dati[$key]['temperatura'] ?>°C
<?php else: ?>
<?= $stato ? 'Acceso' : 'Spento' ?>
<?php endif; ?>
</td>
<td><?= $dati[$key]['ultimo_comando'] ?? 'Mai' ?></td>
<td>
<div class="btn-group btn-group-sm">
<form method="post" class="d-inline">
<input type="hidden" name="device" value="<?= $key ?>">
<button name="accendi" class="btn btn-outline-success btn-sm"
<?= $offline ? 'disabled' : '' ?>>
ON
</button>
<button name="spegni" class="btn btn-outline-danger btn-sm"
<?= $offline ? 'disabled' : '' ?>>
OFF
</button>
</form>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
7. CONFIGURAZIONE PASSO PASSO PER IL PRIMO DISPOSITIVO
Opzione A: Shelly 1 (il più semplice)
- Acquista Shelly 1 (~20€)
- Collegalo alla rete WiFi
- Ottieni l’IP dall’app Shelly
- Modifica
config.php:
php
$dispositivi_reali = [
'shelly1' => [
'nome' => 'Luce Ingresso',
'tipo' => 'interruttore',
'ip' => '192.168.1.XXX', // Sostituisci con IP reale
'model' => 'shelly1'
]
];
- Testa con un browser:
text
http://192.168.1.XXX/relay/0?turn=on http://192.168.1.XXX/relay/0?turn=off http://192.168.1.XXX/status
Opzione B: Presa WiFi TP-Link (Kasa)
- Usa questa libreria: https://github.com/clach04/python-kasa
- Scarica il file PHP adattato: (ti invio via email se vuoi)
- Configura simile a Shelly
Opzione C: ESP8266 personalizzato
- Programma con Arduino IDE
- Usa questo codice base (te lo invio completo)
- Esponi endpoint HTTP semplici
8. SICUREZZA IMPORTANTE
php
// In config.php
define('IP_WHITELIST', [
'192.168.1.0/24', // Solo rete locale
// '85.123.456.789' // Il tuo IP pubblico se accedi da fuori
]);
function checkIP() {
$ip = $_SERVER['REMOTE_ADDR'];
foreach (IP_WHITELIST as $range) {
if (strpos($range, '/')) {
// Controlla subnet
list($subnet, $mask) = explode('/', $range);
if ((ip2long($ip) & ~((1 << (32 - $mask)) - 1)) == ip2long($subnet)) {
return true;
}
} elseif ($ip === $range) {
return true;
}
}
return false;
}
// All'inizio di dashboard.php
if (!checkIP() && $_SERVER['REMOTE_ADDR'] !== '127.0.0.1') {
die('Accesso non autorizzato da questo IP: ' . $_SERVER['REMOTE_ADDR']);
}
RIEPILOGO FILE FINALI:
config.php– Configurazionedashboard.php– Interfaccia principaledispositivi_reali.php– Funzioni controllo dispositiviautomazioni.php– Automazioni (opzionale)dati.json– Dati salvati.htaccess– Sicurezza
Vuoi che ti invii il codice completo per un dispositivo specifico che hai già? (Shelly, TP-Link, ESP8266, etc.) – Posso prepararti un pacchetto pronto da caricare!