354 lines
9.9 KiB
Go
354 lines
9.9 KiB
Go
package main
|
|
|
|
import (
|
|
"app/pkg/datastore"
|
|
"app/pkg/devices"
|
|
"app/pkg/mqtt"
|
|
"app/pkg/telldus"
|
|
daemon "app/pkg/telldus-daemon"
|
|
"encoding/json"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"os/signal"
|
|
"strconv"
|
|
)
|
|
|
|
const (
|
|
httpPort = ":8080"
|
|
)
|
|
|
|
var mqttClient *mqtt.Client
|
|
var store *datastore.DataStore
|
|
var daemonMgr *daemon.Manager
|
|
var eventMgr *devices.EventManager
|
|
|
|
const maxEvents = 1000
|
|
|
|
func main() {
|
|
// Initialize daemon manager
|
|
daemonMgr = daemon.New()
|
|
if err := daemonMgr.Start(); err != nil {
|
|
log.Fatalf("Failed to start telldusd: %v", err)
|
|
}
|
|
defer daemonMgr.Stop()
|
|
|
|
// Initialize Telldus
|
|
telldus.Init()
|
|
defer telldus.Close()
|
|
|
|
// Initialize DataStore
|
|
var err error
|
|
store, err = datastore.New("./db/telldus.db")
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
defer store.Close()
|
|
|
|
// Sync devices and sensors
|
|
syncer := devices.NewSyncer(store)
|
|
if err := syncer.SyncDevices(); err != nil {
|
|
log.Printf("Error syncing devices: %v", err)
|
|
}
|
|
if err := syncer.SyncSensors(); err != nil {
|
|
log.Printf("Error syncing sensors: %v", err)
|
|
}
|
|
|
|
// Device reload function for config file changes
|
|
reloadDevices := func() error {
|
|
log.Println("Configuration file changed, restarting telldusd...")
|
|
if err := daemonMgr.Restart(); err != nil {
|
|
log.Printf("Failed to restart telldusd: %v", err)
|
|
return err
|
|
}
|
|
log.Println("Reloading devices and sensors after config change...")
|
|
if err := syncer.SyncDevices(); err != nil {
|
|
log.Printf("Error syncing devices: %v", err)
|
|
return err
|
|
}
|
|
if err := syncer.SyncSensors(); err != nil {
|
|
log.Printf("Error syncing sensors: %v", err)
|
|
return err
|
|
}
|
|
if err := mqttClient.PublishAllDiscovery(); err != nil {
|
|
log.Printf("Error republishing discovery: %v", err)
|
|
return err
|
|
}
|
|
if err := mqttClient.SubscribeToDeviceCommands(); err != nil {
|
|
log.Printf("Error resubscribing to commands: %v", err)
|
|
return err
|
|
}
|
|
log.Println("Successfully reloaded devices and sensors")
|
|
return nil
|
|
}
|
|
|
|
// Start watching config file
|
|
configPath := "/etc/tellstick.conf"
|
|
watcher := daemon.NewWatcher(configPath, reloadDevices)
|
|
go func() {
|
|
if err := watcher.Watch(); err != nil {
|
|
log.Printf("Config watcher error: %v", err)
|
|
}
|
|
}()
|
|
|
|
// Initialize MQTT
|
|
mqttConfig := mqtt.Config{
|
|
BrokerURL: os.Getenv("MQTT_URL"),
|
|
Username: os.Getenv("MQTT_USER"),
|
|
Password: os.Getenv("MQTT_PASSWORD"),
|
|
}
|
|
mqttClient, err = mqtt.New(mqttConfig, store)
|
|
if err != nil {
|
|
log.Fatalf("Failed to connect to MQTT: %v", err)
|
|
}
|
|
defer mqttClient.Close()
|
|
|
|
// Publish Home Assistant discovery
|
|
if err := mqttClient.PublishAllDiscovery(); err != nil {
|
|
log.Printf("Error publishing discovery: %v", err)
|
|
}
|
|
|
|
// Subscribe to command topics
|
|
if err := mqttClient.SubscribeToDeviceCommands(); err != nil {
|
|
log.Printf("Error subscribing to commands: %v", err)
|
|
}
|
|
|
|
// List devices and sensors
|
|
syncer.ListDevices()
|
|
syncer.ListSensors()
|
|
|
|
// Initialize event manager and register callbacks
|
|
eventMgr = devices.NewEventManager(store, mqttClient, maxEvents)
|
|
eventMgr.RegisterCallbacks()
|
|
|
|
// Setup graceful shutdown
|
|
c := make(chan os.Signal, 1)
|
|
signal.Notify(c, os.Interrupt)
|
|
go func() {
|
|
<-c
|
|
log.Println("Shutting down gracefully...")
|
|
mqttClient.Close()
|
|
telldus.Close()
|
|
daemonMgr.Stop()
|
|
os.Exit(0)
|
|
}()
|
|
|
|
// Start HTTP server
|
|
log.Println("Server starting on", httpPort)
|
|
log.Fatal(http.ListenAndServe(httpPort, setupRoutes()))
|
|
}
|
|
|
|
func getRawEvents(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(eventMgr.GetRawEvents())
|
|
}
|
|
|
|
func getSensorEvents(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(eventMgr.GetSensorEvents())
|
|
}
|
|
|
|
func getPotentialDevices(w http.ResponseWriter, r *http.Request) {
|
|
devices := []*datastore.PotentialDevice{}
|
|
for device := range store.ListPotentialDevices() {
|
|
devices = append(devices, device)
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(devices)
|
|
}
|
|
|
|
func renameDevice(w http.ResponseWriter, r *http.Request) {
|
|
idStr := r.PathValue("id")
|
|
id, err := strconv.Atoi(idStr)
|
|
if err != nil {
|
|
http.Error(w, "Invalid device ID", http.StatusBadRequest)
|
|
return
|
|
}
|
|
var req struct {
|
|
Name string `json:"name"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
http.Error(w, "Bad request", http.StatusBadRequest)
|
|
return
|
|
}
|
|
if err := store.UpdateDeviceName(id, req.Name); err != nil {
|
|
http.Error(w, "Database error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
// Republish discovery for this device
|
|
if err := mqttClient.PublishDeviceDiscovery(id); err != nil {
|
|
log.Printf("Error republishing device discovery: %v", err)
|
|
}
|
|
w.WriteHeader(http.StatusOK)
|
|
}
|
|
|
|
func renameSensor(w http.ResponseWriter, r *http.Request) {
|
|
sensorIdStr := r.PathValue("sensor_id")
|
|
sensorId, err := strconv.Atoi(sensorIdStr)
|
|
if err != nil {
|
|
http.Error(w, "Invalid sensor ID", http.StatusBadRequest)
|
|
return
|
|
}
|
|
var req struct {
|
|
Name string `json:"name"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
http.Error(w, "Bad request", http.StatusBadRequest)
|
|
return
|
|
}
|
|
sensor, err := store.GetSensor(sensorId)
|
|
if err != nil {
|
|
http.Error(w, "Sensor not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
if err := store.UpdateSensorName(sensorId, req.Name); err != nil {
|
|
http.Error(w, "Database error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
// Republish discovery for this sensor
|
|
if err := mqttClient.PublishSensorDiscovery(sensor.Protocol, sensor.Model, sensor.ID); err != nil {
|
|
log.Printf("Error republishing sensor discovery: %v", err)
|
|
}
|
|
w.WriteHeader(http.StatusOK)
|
|
}
|
|
|
|
func hideSensor(w http.ResponseWriter, r *http.Request) {
|
|
sensorIdStr := r.PathValue("sensor_id")
|
|
sensorId, err := strconv.Atoi(sensorIdStr)
|
|
if err != nil {
|
|
http.Error(w, "Invalid sensor ID", http.StatusBadRequest)
|
|
return
|
|
}
|
|
if err := store.SetSensorHidden(sensorId, true); err != nil {
|
|
http.Error(w, "Database error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusOK)
|
|
}
|
|
|
|
func unhideSensor(w http.ResponseWriter, r *http.Request) {
|
|
sensorIdStr := r.PathValue("sensor_id")
|
|
sensorId, err := strconv.Atoi(sensorIdStr)
|
|
if err != nil {
|
|
http.Error(w, "Invalid sensor ID", http.StatusBadRequest)
|
|
return
|
|
}
|
|
if err := store.SetSensorHidden(sensorId, false); err != nil {
|
|
http.Error(w, "Database error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusOK)
|
|
}
|
|
|
|
func setupRoutes() *http.ServeMux {
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/api/devices", getDevices)
|
|
mux.HandleFunc("POST /api/devices/{id}/turnon", turnOnDevice)
|
|
mux.HandleFunc("POST /api/devices/{id}/learn", learnDevice)
|
|
mux.HandleFunc("POST /api/devices/{id}/turnoff", turnOffDevice)
|
|
mux.HandleFunc("/api/sensors", getSensors)
|
|
mux.HandleFunc("/api/devices/{id}", getDevice)
|
|
mux.HandleFunc("PUT /api/devices/{id}", renameDevice)
|
|
mux.HandleFunc("PUT /api/sensors/{sensor_id}", renameSensor)
|
|
mux.HandleFunc("PUT /api/sensors/{sensor_id}/hide", hideSensor)
|
|
mux.HandleFunc("PUT /api/sensors/{sensor_id}/unhide", unhideSensor)
|
|
mux.HandleFunc("/api/sensors/{sensor_id}", getSensor)
|
|
mux.HandleFunc("/api/events/raw", getRawEvents)
|
|
mux.HandleFunc("/api/events/sensor", getSensorEvents)
|
|
mux.HandleFunc("/api/potential_devices", getPotentialDevices)
|
|
// Serve static files for the frontend
|
|
mux.Handle("/", http.FileServer(http.Dir("./dist")))
|
|
return mux
|
|
}
|
|
func getDevices(w http.ResponseWriter, r *http.Request) {
|
|
devices := []*datastore.Device{}
|
|
for device := range store.ListDevices() {
|
|
devices = append(devices, device)
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(devices)
|
|
}
|
|
|
|
type CommandResult struct {
|
|
Success bool `json:"success"`
|
|
Message string `json:"message,omitempty"`
|
|
Code int `json:"code,omitempty"`
|
|
}
|
|
|
|
func turnOnDevice(w http.ResponseWriter, r *http.Request) {
|
|
idStr := r.PathValue("id")
|
|
id, err := strconv.Atoi(idStr)
|
|
if err != nil {
|
|
http.Error(w, "Invalid device ID", http.StatusBadRequest)
|
|
return
|
|
}
|
|
status := telldus.TurnOn(id)
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(CommandResult{Success: status == 0, Message: "turned on", Code: status})
|
|
}
|
|
|
|
func learnDevice(w http.ResponseWriter, r *http.Request) {
|
|
idStr := r.PathValue("id")
|
|
id, err := strconv.Atoi(idStr)
|
|
if err != nil {
|
|
http.Error(w, "Invalid device ID", http.StatusBadRequest)
|
|
return
|
|
}
|
|
status := telldus.Learn(id)
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(CommandResult{Success: status == 0, Message: "learning started", Code: status})
|
|
}
|
|
|
|
func turnOffDevice(w http.ResponseWriter, r *http.Request) {
|
|
idStr := r.PathValue("id")
|
|
id, err := strconv.Atoi(idStr)
|
|
if err != nil {
|
|
http.Error(w, "Invalid device ID", http.StatusBadRequest)
|
|
return
|
|
}
|
|
status := telldus.TurnOff(id)
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(CommandResult{Success: status == 0, Message: "turned off", Code: status})
|
|
}
|
|
|
|
func getSensors(w http.ResponseWriter, r *http.Request) {
|
|
sensors := []*datastore.Sensor{}
|
|
for sensor := range store.ListSensors() {
|
|
sensors = append(sensors, sensor)
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(sensors)
|
|
}
|
|
|
|
func getDevice(w http.ResponseWriter, r *http.Request) {
|
|
idStr := r.PathValue("id")
|
|
id, err := strconv.Atoi(idStr)
|
|
if err != nil {
|
|
http.Error(w, "Invalid device ID", http.StatusBadRequest)
|
|
return
|
|
}
|
|
device, err := store.GetDevice(id)
|
|
if err != nil {
|
|
http.Error(w, "Device not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(device)
|
|
}
|
|
|
|
func getSensor(w http.ResponseWriter, r *http.Request) {
|
|
sensorIdStr := r.PathValue("sensor_id")
|
|
sensorId, err := strconv.Atoi(sensorIdStr)
|
|
if err != nil {
|
|
http.Error(w, "Invalid sensor ID", http.StatusBadRequest)
|
|
return
|
|
}
|
|
sensor, err := store.GetSensor(sensorId)
|
|
if err != nil {
|
|
http.Error(w, "Sensor not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(sensor)
|
|
}
|