Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,10 @@ coverage.xml
.pytest_cache/
cover/

# Translations
# Translations (gettext). Keep Modelica sources.
*.mo
!modelica/
!modelica/*.mo
*.pot

# Django stuff:
Expand Down Expand Up @@ -177,3 +179,20 @@ data/state.json

# Go binaries
/simulator

# OpenModelica / FMI build products (compile locally with `make fmu`)
modelica/*.fmu
modelica/Site4_*
modelica/*.inst.mo
modelica/*.libs
modelica/*.makefile
modelica/*.json
modelica/*.html
modelica/*.c
modelica/*.h
modelica/*.o
modelica/*.xml
modelica/*.log
modelica/*.exe
modelica/*.so

7 changes: 6 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ The app supports two modes controlled by the `--desktop` flag:
- Pixii PowerShaper
- FerroAmp EnergyHub
- SDM630 Energy Meter
- Generic SG Ready heat pump (site plant)

## API Endpoints

Expand All @@ -107,7 +108,7 @@ HTTP API (port 80 in Docker, 8762 in desktop mode):
```
GET / # Dashboard UI
GET /api/simulators # List all simulators
POST /api/simulators # Create simulator {type: "inverter"|"energy_meter"|"v2x_charger"|"ocpp_charger"}
POST /api/simulators # Create simulator {type: "inverter"|"energy_meter"|"heat_pump"|"v2x_charger"|"ocpp_charger"}
DELETE /api/simulators/{id} # Delete simulator
GET /api/simulators/{id} # Get simulator details
POST /api/simulators/{id}/config # Update serial/slave ID
Expand All @@ -119,6 +120,10 @@ POST /api/simulators/{id}/automation/stop # Stop data generation
POST /api/simulators/{id}/reset # Reset to defaults
GET /api/sites # List all sites
POST /api/sites # Create site
GET /api/facilities # List facility presets
GET /api/sites/{id}/facility # Get site facility
POST /api/sites/{id}/facility # Apply facility preset (plant + devices)
GET /api/sites/{id}/plant # Live electrical + thermal snapshot
GET /api/system # Get system info (local IP)
GET /api/version # Get app version
GET /api/update/check # Check for updates
Expand Down
14 changes: 13 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
.PHONY: up down restart build logs clean test env \
build-macos build-linux build-linux-server build-windows \
run-macos package help
run-macos package help fmu fmu-clean

# Auto-detect host IP for Mac (en0 = WiFi, en1 = Ethernet)
HOST_IP ?= $(shell ipconfig getifaddr en0 2>/dev/null || ipconfig getifaddr en1 2>/dev/null || echo "")
Expand Down Expand Up @@ -32,6 +32,17 @@ clean:
test:
go test ./...

# Compile modelica/SiteEnergy.Site4 to an FMI 2.0 co-simulation FMU.
# Requires OpenModelica (`omc`). The simulator loads it via siteplant.Open.
fmu:
@command -v omc >/dev/null || (echo "omc not found. Install OpenModelica: https://openmodelica.org" && exit 1)
cd modelica && omc export.mos

fmu-clean:
cd modelica && rm -f Site4.fmu Site4_*.c Site4_*.h Site4_*.o Site4_*.json \
Site4_*.libs Site4_*.makefile Site4_*.log Site4.inst.mo index.html \
Site4_FMU.* 2>/dev/null || true

# Build macOS desktop app (native, requires macOS)
build-macos:
@echo "Building macOS desktop app..."
Expand Down Expand Up @@ -126,5 +137,6 @@ help:
@echo ""
@echo "Other:"
@echo " make test - Run Go tests"
@echo " make fmu - Compile SiteEnergy.Site4 to FMI 2.0 CS (needs omc)"
@echo " make package - Create release packages"
@echo " make clean - Remove build artifacts"
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ docker compose up --build
- **Site Management** - Group simulators into sites with energy meters
- **Device Profiles** - Sungrow, SolarEdge, Fronius, Huawei, and more
- **Time Acceleration** - Compress 24 hours into minutes for testing
- **Physical plant** - Site AC bus with conserved power and battery SoC. Modelica in `modelica/SiteEnergy.mo`; compile with `make fmu` and plug in via `siteplant`. Sketch: [docs/SITE_PLANT_SKETCH.md](docs/SITE_PLANT_SKETCH.md)
- **Site simulator** - Facility presets (ASHP/GSHP, occupancy, DHW) behind Modbus facades so an EMS can treat the process as a real site. [docs/SITE_SIMULATOR.md](docs/SITE_SIMULATOR.md)
- **Real-time Logging** - See every protocol operation as it happens

## Supported Profiles
Expand All @@ -79,6 +81,7 @@ docker compose up --build
| deye | Deye hybrid inverter |
| pixii-powershaper | Pixii PowerShaper |
| ferroamp | FerroAmp EnergyHub |
| heat-pump | Generic SG Ready heat pump (site plant) |
| sdm630 | SDM630 Energy Meter |

> **Disclaimer:** Device Simulator is an independent testing tool and is **not affiliated with, endorsed by, or sponsored by** any of the manufacturers listed above. Product and company names are trademarks of their respective owners and are used only to identify the device behavior being simulated.
Expand Down
54 changes: 45 additions & 9 deletions cmd/simulator/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (

"github.com/srcfl/device-simulator/internal/modbus"
"github.com/srcfl/device-simulator/internal/modbus/devices"
"github.com/srcfl/device-simulator/internal/plant"
"github.com/srcfl/device-simulator/internal/settings"
"github.com/wailsapp/wails/v2/pkg/runtime"
)
Expand Down Expand Up @@ -732,6 +733,7 @@ func (a *App) GetSites() []map[string]interface{} {
"time_multiplier": site.TimeMultiplier,
"sim_running": site.SimRunning,
"scenario": site.Scenario,
"facility_id": site.FacilityID,
}
p1Mode := P1ModeDirect
if site.P1Buffer != nil {
Expand Down Expand Up @@ -818,6 +820,8 @@ func (a *App) GetSite(id int) (map[string]interface{}, error) {
"phase_loads": site.SitePhaseLoads,
"load_scenario": site.LoadScenario,
"load_linked": site.LoadLinked,
"facility_id": site.FacilityID,
"plant": site.lastPlant,
}

meterID := site.MeterID
Expand Down Expand Up @@ -1181,15 +1185,15 @@ func (a *App) CreateSimulatorInSite(siteId int, simType string) (map[string]inte
a.emitSiteUpdate()

return map[string]interface{}{
"id": sim.ID,
"serial": sim.Serial,
"protocol": sim.Protocol,
"category": sim.Category,
"port": sim.Port,
"running": sim.Running,
"site_id": siteId,
"device_on": sim.DeviceOn,
"mdns_hostname": mdnsAdvertiser.ActiveHostname(sim.ID),
"id": sim.ID,
"serial": sim.Serial,
"protocol": sim.Protocol,
"category": sim.Category,
"port": sim.Port,
"running": sim.Running,
"site_id": siteId,
"device_on": sim.DeviceOn,
"mdns_hostname": mdnsAdvertiser.ActiveHostname(sim.ID),
}, nil
}

Expand Down Expand Up @@ -1547,3 +1551,35 @@ func (a *App) SetSiteLoadScenario(id int, scenario string) (map[string]interface

return result, nil
}

// GetFacilities returns site physics templates (heat pumps, loads, envelope).
func (a *App) GetFacilities() []plant.Facility {
return plant.Presets()
}

// ApplySiteFacility instantiates a facility preset on a site (devices + plant).
func (a *App) ApplySiteFacility(id int, preset string) (map[string]interface{}, error) {
site := getSite(id)
if site == nil {
return nil, fmt.Errorf("site %d not found", id)
}
if err := applyFacilityPreset(site, preset); err != nil {
return nil, err
}
if err := startSiteSimulationWithServers(site); err != nil {
log.Printf("[Site %d] start after facility: %v", id, err)
}
markStateDirty()
a.emitSimulatorUpdate()
a.emitSiteUpdate()
return sitePlantJSON(site), nil
}

// GetSitePlant returns the live physics snapshot for the schematic.
func (a *App) GetSitePlant(id int) (map[string]interface{}, error) {
site := getSite(id)
if site == nil {
return nil, fmt.Errorf("site %d not found", id)
}
return sitePlantJSON(site), nil
}
Loading
Loading