Open h0bb3 opened 1 month ago
One interesting aspect is that as the tasks are now threads of their own the Harvest can actually be simplified (as the non blocking task requirement is no more). This would allow for some other design alternatives. I.e we could push some parts of the retry functionality to the inverter (or some new middle man class i.e. InverterHarvest) That is something not also a Task (as we do not need to return but can simply sleep).
In fact the harvest can be one perpetual task with e.g. a while true loop in it. Though then we do loose the ability to control, i.e kill, the thread if we do not specifically create something new here in this extreme case.
But the Harvest task could essentially be
try:
backoff_time, data = self.der.harvest()
self.time += backoff_time
# harvest transport stuff
return self
except:
# ... possibly no need to do anything here
This is basically the current design
classDiagram
Inverter <|-- ModbusTCP
Inverter <|-- ModbusRTU
Inverter <|-- SolarmanTCP
TaskScheduler --> "*" ITask
ITask <|.. Task
Task <|-- Harvest
Task --> BlackBoard
BlackBoard--> "*" Inverter
Harvest --> Inverter
Harvest ..> HarvestTransport
Task <|-- HarvestTransport
class Task {
event_time:int
}
class ITask {
<<interface>>
}
latest iteration
classDiagram
namespace der_hardware_protocol {
class ICom {
<<interface>>
+connect()*
+read_harvest_data()*
+get_harvest_data_type()*
+get_hw_id()*
}
class HarvestDataType {
<<enumeration>>
MODBUS_REGISTERS
JSON_OBJECT
}
class ModbusTCP {
+connect()
}
class ModbusRTU {
+connect()
}
class MagickAPI
class Solarman {
}
class Sunspec
class hwc_factory {
create_from_string(...) ICom
create_from_json(...)
}
}
hwc_factory ..> ModbusTCP
hwc_factory ..> ModbusRTU
hwc_factory ..> Solarman
hwc_factory ..> MagickAPI
hwc_factory ..> Sunspec
ICom ..> HarvestDataType
ModbusTCP ..|> ICom
ModbusRTU ..|> ICom
Solarman ..|> ICom
MagickAPI ..|> ICom
Sunspec ..|> ICom
class DER {
+read_harvest_data()*
+get_hw_id()*
}
class IConsumer {
<<interface>>
+consume(percent)*
}
class IProducer {
<<interface>>
+produce(percent)*
}
class IStorage {
<<interface>>
+stateOfCharge()*
+getConfig()*
}
class Solar {
+produce()
}
namespace tasks {
class ITask {
<<interface>>
}
class Task {
}
class ControlProduction {
}
class Harvest
class OpenDERTask
class OpenDERPerpetualTask
}
namespace der {
class DER
class IProducer
class IConsumer
class IStorage
class Solar
class PoolHeater
class Battery
}
BlackBoard --> "*"IConsumer
BlackBoard --> "*"IProducer
BlackBoard --> "*"IStorage
Task ..|> ITask
Task --> BlackBoard
Harvest --|> Task
OpenDERPerpetualTask --|> Task
OpenDERTask --|> Task
ControlProduction --|> Task
ControlProduction --> IProducer
Harvest --> DER
OpenDERPerpetualTask --> DER
OpenDERTask --> DER
TaskQueue --> "*" ITask
DER <|-- PoolHeater
DER <|-- Battery
IStorage <|.. Battery
IConsumer <|.. PoolHeater
IProducer <|.. Solar
DER <|-- Solar
DER --> "icomm" ICom :
I will suppose this issue should be divided into several smaller ones to not create a mega merge and break everything.
The hw_factory side is partially implemented now. Consider making ModbusRTU/TCP/Solarman use SunSpecModbusClientDevice for "normal" modbus operations, e.g. read/write registers.
Updated version:
classDiagram
namespace der_hardware_protocol {
class ICom {
<<interface>>
+connect()*
+read_harvest_data()*
+get_harvest_data_type()*
+get_hw_id()*
}
class HarvestDataType {
<<enumeration>>
MODBUS_REGISTERS
JSON_OBJECT
}
class ModbusTCP
class ModbusRTU
class MagicAPI
class Solarman
class SunSpec
class hwc_factory {
create_from_string(...) ICom
create_from_json(...)
}
class ModbusDevice
class SunSpecModbusClientDevice
}
hwc_factory ..> ModbusTCP
hwc_factory ..> ModbusRTU
hwc_factory ..> Solarman
hwc_factory ..> MagicAPI
hwc_factory ..> SunSpec
ICom ..> HarvestDataType
ModbusTCP --|> ModbusDevice
ModbusRTU --|> ModbusDevice
Solarman --|> ModbusDevice
SunSpec --|> SunSpecModbusClientDevice
SunSpecModbusClientDevice ..|> ICom
ModbusDevice ..|> ICom
MagicAPI ..|> ICom
class DER {
+read_harvest_data()*
+get_hw_id()*
}
class IConsumer {
<<interface>>
+consume(percent)*
}
class IProducer {
<<interface>>
+produce(percent)*
}
class IStorage {
<<interface>>
+stateOfCharge()*
+getConfig()*
}
class Solar {
+produce()
}
namespace tasks {
class ITask {
<<interface>>
}
class Task {
}
class ControlProduction {
}
class Harvest
class OpenDERTask
class OpenDERPerpetualTask
}
namespace der {
class DER
class IProducer
class IConsumer
class IStorage
class Solar
class PoolHeater
class Battery
}
BlackBoard --> "*"IConsumer
BlackBoard --> "*"IProducer
BlackBoard --> "*"IStorage
Task ..|> ITask
Task --> BlackBoard
Harvest --|> Task
OpenDERPerpetualTask --|> Task
OpenDERTask --|> Task
ControlProduction --|> Task
ControlProduction --> IProducer
Harvest --> DER
OpenDERPerpetualTask --> DER
OpenDERTask --> DER
TaskQueue --> "*" ITask
DER <|-- PoolHeater
DER <|-- Battery
IStorage <|.. Battery
IConsumer <|.. PoolHeater
IProducer <|.. Solar
DER <|-- Solar
DER --> "icomm" ICom :
related to #177
the fw would need to support multiple DERs. Eg. PV and Battery. This should be flexible enough so that any DER can be added. At the highest level there are Production DERs (PV), Consumption DERs (heater) and combination PruductonAndConsumption (battery).
Maybe we can find some common interface(s) and work with that. DERs should support harvesting data and controll (depending on DER type). The underlying mechanism to do this is currently via an inverter -> modbus tcp. So this seems like another layer of abstraction on top of things?
This would mean the harvester works with a DER and not an inverter. The DER then implements the actual harvesting.
An open issue is if a user has both Solar and Battery, will it be able to participate in controll contracts of both kinds or is the control at the level of the "facility"?