package eebus import ( "crypto/tls" "errors" "fmt" "net" "slices" "strconv" "sync" "time" "dario.cat/mergo" eebusapi "github.com/enbility/eebus-go/api" service "github.com/enbility/eebus-go/service" ucapi "github.com/enbility/eebus-go/usecases/api" "github.com/enbility/eebus-go/usecases/cem/evcc" "github.com/enbility/eebus-go/usecases/cem/evcem" "github.com/enbility/eebus-go/usecases/cem/evsecc" "github.com/enbility/eebus-go/usecases/cem/evsoc" "github.com/enbility/eebus-go/usecases/cem/opev" "github.com/enbility/eebus-go/usecases/cem/oscev" "github.com/enbility/eebus-go/usecases/cs/lpc" "github.com/enbility/eebus-go/usecases/cs/lpp" eglpc "github.com/enbility/eebus-go/usecases/eg/lpc" "github.com/enbility/eebus-go/usecases/ma/mgcp" "github.com/enbility/eebus-go/usecases/ma/mpc" shipapi "github.com/enbility/ship-go/api" "github.com/enbility/ship-go/mdns" shiputil "github.com/enbility/ship-go/util" spineapi "github.com/enbility/spine-go/api" "github.com/enbility/spine-go/model" "github.com/evcc-io/evcc/util" "github.com/evcc-io/evcc/util/machine" ) type Device interface { Connect(connected bool) UseCaseEvent(device spineapi.DeviceRemoteInterface, entity spineapi.EntityRemoteInterface, event eebusapi.EventType) } // Customer Energy Management type CustomerEnergyManagement struct { EvseCC ucapi.CemEVSECCInterface EvCC ucapi.CemEVCCInterface EvCem ucapi.CemEVCEMInterface EvSoc ucapi.CemEVSOCInterface OpEV ucapi.CemOPEVInterface OscEV ucapi.CemOSCEVInterface } // Controllable System type ControllableSystem struct { ucapi.CsLPCInterface ucapi.CsLPPInterface } // Monitoring Appliance type MonitoringAppliance struct { ucapi.MaMGCPInterface ucapi.MaMPCInterface } // Energy Guard type EnergyGuard struct { ucapi.EgLPCInterface } type EEBus struct { service eebusapi.ServiceInterface cem CustomerEnergyManagement cs ControllableSystem ma MonitoringAppliance eg EnergyGuard mux sync.Mutex log *util.Logger ski string clients map[string][]Device } var Instance *EEBus func NewServer(other Config) (*EEBus, error) { cc := Config{ URI: ":4712", } if err := mergo.Merge(&cc, other, mergo.WithOverride); err != nil { return nil, err } log := util.NewLogger("eebus") protectedID := machine.ProtectedID("evcc-eebus") serial := fmt.Sprintf("%s-%0x", "EVCC", protectedID[:8]) if len(cc.ShipID) != 0 { serial = cc.ShipID } certificate, err := tls.X509KeyPair([]byte(cc.Certificate.Public), []byte(cc.Certificate.Private)) if err != nil { return nil, err } _, portValue, err := net.SplitHostPort(cc.URI) if err != nil { return nil, err } port, err := strconv.Atoi(portValue) if err != nil { return nil, err } // TODO: get the voltage from the site configuration, err := eebusapi.NewConfiguration( BrandName, BrandName, Model, serial, model.DeviceTypeTypeEnergyManagementSystem, []model.EntityTypeType{model.EntityTypeTypeCEM}, port, certificate, time.Second*4, ) if err != nil { return nil, err } // use avahi if available, otherwise use go based zeroconf configuration.SetMdnsProviderSelection(mdns.MdnsProviderSelectionAll) // for backward compatibility configuration.SetAlternateMdnsServiceName(DeviceCode) configuration.SetAlternateIdentifier(serial) configuration.SetInterfaces(cc.Interfaces) ski, err := SkiFromCert(certificate) if err != nil { return nil, err } c := &EEBus{ log: log, ski: ski, clients: make(map[string][]Device), } c.service = service.NewService(configuration, c) c.service.SetLogging(c) if err := c.service.Setup(); err != nil { return nil, err } localEntity := c.service.LocalDevice().EntityForType(model.EntityTypeTypeCEM) // evse c.cem = CustomerEnergyManagement{ EvseCC: evsecc.NewEVSECC(localEntity, c.ucCallback), EvCC: evcc.NewEVCC(c.service, localEntity, c.ucCallback), EvCem: evcem.NewEVCEM(c.service, localEntity, c.ucCallback), OpEV: opev.NewOPEV(localEntity, c.ucCallback), OscEV: oscev.NewOSCEV(localEntity, c.ucCallback), EvSoc: evsoc.NewEVSOC(localEntity, c.ucCallback), } // controllable system c.cs = ControllableSystem{ CsLPCInterface: lpc.NewLPC(localEntity, c.ucCallback), CsLPPInterface: lpp.NewLPP(localEntity, c.ucCallback), } // monitoring appliance c.ma = MonitoringAppliance{ MaMGCPInterface: mgcp.NewMGCP(localEntity, c.ucCallback), MaMPCInterface: mpc.NewMPC(localEntity, c.ucCallback), } // energy guard c.eg = EnergyGuard{ EgLPCInterface: eglpc.NewLPC(localEntity, c.ucCallback), } // register use cases for _, uc := range []eebusapi.UseCaseInterface{ c.cem.EvseCC, c.cem.EvCC, c.cem.EvCem, c.cem.OpEV, c.cem.OscEV, c.cem.EvSoc, c.cs.CsLPCInterface, c.cs.CsLPPInterface, c.ma.MaMGCPInterface, c.ma.MaMPCInterface, c.eg.EgLPCInterface, } { c.service.AddUseCase(uc) } return c, nil } func (c *EEBus) RegisterDevice(ski, ip string, device Device) error { ski = shiputil.NormalizeSKI(ski) c.log.TRACE.Printf("registering ski: %s", ski) if ski == c.ski { return errors.New("device ski can not be identical to host ski") } if len(ip) > 0 { c.service.RemoteServiceForSKI(ski).SetIPv4(ip) } c.service.RegisterRemoteSKI(ski) c.mux.Lock() defer c.mux.Unlock() c.clients[ski] = append(c.clients[ski], device) return nil } func (c *EEBus) UnregisterDevice(ski string, device Device) { ski = shiputil.NormalizeSKI(ski) c.log.TRACE.Printf("unregistering ski: %s", ski) c.service.UnregisterRemoteSKI(ski) c.mux.Lock() defer c.mux.Unlock() if idx := slices.Index(c.clients[ski], device); idx != -1 { c.clients[ski] = slices.Delete(c.clients[ski], idx, idx+1) } } func (c *EEBus) CustomerEnergyManagement() *CustomerEnergyManagement { return &c.cem } func (c *EEBus) ControllableSystem() *ControllableSystem { return &c.cs } func (c *EEBus) MonitoringAppliance() *MonitoringAppliance { return &c.ma } func (c *EEBus) EnergyGuard() *EnergyGuard { return &c.eg } func (c *EEBus) Run() { c.service.Start() } func (c *EEBus) Shutdown() { c.service.Shutdown() } // Use case callback func (c *EEBus) ucCallback(ski string, device spineapi.DeviceRemoteInterface, entity spineapi.EntityRemoteInterface, event eebusapi.EventType) { c.mux.Lock() defer c.mux.Unlock() c.log.DEBUG.Printf("ski %s event %s", ski, event) if clients, ok := c.clients[ski]; ok { for _, client := range clients { client.UseCaseEvent(device, entity, event) } } } // EEBUSServiceHandler func (c *EEBus) connect(ski string, connected bool) { action := map[bool]string{true: "connected", false: "disconnected"}[connected] c.log.DEBUG.Printf("ski %s %s", ski, action) c.mux.Lock() defer c.mux.Unlock() if clients, ok := c.clients[ski]; ok { for _, client := range clients { client.Connect(connected) } } } func (c *EEBus) RemoteSKIConnected(service eebusapi.ServiceInterface, ski string) { c.connect(ski, true) } func (c *EEBus) RemoteSKIDisconnected(service eebusapi.ServiceInterface, ski string) { c.connect(ski, false) } // report all currently visible EEBUS services // this is needed to provide an UI for pairing with other devices // if not all incoming pairing requests should be accepted func (c *EEBus) VisibleRemoteServicesUpdated(service eebusapi.ServiceInterface, entries []shipapi.RemoteService) { } // Provides the SHIP ID the remote service reported during the handshake process // This needs to be persisted and passed on for future remote service connections // when using `PairRemoteService` func (c *EEBus) ServiceShipIDUpdate(ski string, shipdID string) {} // Provides the current pairing state for the remote service // This is called whenever the state changes and can be used to // provide user information for the pairing/connection process func (c *EEBus) ServicePairingDetailUpdate(ski string, detail *shipapi.ConnectionStateDetail) { if detail.State() != shipapi.ConnectionStateReceivedPairingRequest { return } c.mux.Lock() defer c.mux.Unlock() if clients, ok := c.clients[ski]; !ok || len(clients) == 0 { // this is an unknown SKI, so deny pairing c.service.CancelPairingWithSKI(ski) } } // EEBUS Logging interface func (c *EEBus) Trace(args ...any) { c.log.TRACE.Println(args...) } func (c *EEBus) Tracef(format string, args ...any) { c.log.TRACE.Printf(format, args...) } func (c *EEBus) Debug(args ...any) { c.log.DEBUG.Println(args...) } func (c *EEBus) Debugf(format string, args ...any) { c.log.DEBUG.Printf(format, args...) } func (c *EEBus) Info(args ...any) { c.log.INFO.Println(args...) } func (c *EEBus) Infof(format string, args ...any) { c.log.INFO.Printf(format, args...) } func (c *EEBus) Error(args ...any) { c.log.ERROR.Println(args...) } func (c *EEBus) Errorf(format string, args ...any) { c.log.ERROR.Printf(format, args...) }