本章节主要讲的是在充电站首次连接云平台或者重启后要去连接云平台的动作,也就是充电站的注册。一共有12个case,B01-B12,我不一定会详尽的介绍所有的case或者是事件,但是会尽量实现每一个case所讲述的内容。
B01 - Cold Boot Charging Station(冷启动充电站)
本用例的目的是使正在通电的充电站能够在 CSMS (平台/服务端)上注册,并提供正确的状态信息。为了能够控制连接到 CSMS 的充电站,充电站需要发送BootNotificationRequest。该请求包含充电站的一些一般信息。
根据时序图我们可以了解到我们应该做的就是在冷启动的时候发送相应的boot_notification,而在python的ocpp库中已经定义好了相应的PayLoad,我们直接调用和填写就可以。
在protocols/ocpp项目中已经给出了一个简单的bootnotification的定义,代码如下:
async def send_boot_notification(self):
request = call.BootNotificationPayload(
charging_station={"model": "Wallbox XYZ", "vendor_name": "anewone"},
reason="PowerUp",
)
response = await self.call(request)
if response.status == "Accepted":
print("Connected to central system.")
await self.send_heartbeat(response.interval)
async是一个异步处理的库,具体想要了解可以自行查阅资料去了解一下,在这里我就不过多赘述了。这样做是为了在发送notification之后,我不想让程序一直等待结果,因为还有其他的程序需要运行。
代码中可以看到,在创建request对象的时候,直接采用了call.BootNotificationPayload的方法去进行定义JSON中具体的变量以及值,我们在源码中也可以看到对于BootNotificationPayload的定义如下:
@dataclass
class BootNotificationPayload:
charging_station: Dict
reason: str
custom_data: Optional[Dict[str, Any]] = None
@dataclass是python中的数据类,根据OCPP2.0.1协议中我们也可以看到定义的数据类型如下:
所以在这里,我们填写相应的字段和内容即可,后续更深入的字段请移步OCPP2.0.1协议,文件在这里有,或者也可以去官网下载和查看。
实际上,官方框架给出的只是一种情况,也就是首次上电与平台绑定的时候才会应用,所以需要做一个小小的调整,调整后如下:
async def send_boot_notification(self, charging_station, reason,):
request = call.BootNotificationPayload(
charging_station=charging_station,
reason=reason
)
response = await self.call(request)
if response.status == "Accepted":
print("Connected to central system.")
await self.send_heartbeat(response.interval)
当然,只是简单的可以进行自定义参数了而已,并不是很大的改动。如果云平台给予我们回复为Accepted,那则是云平台接受了我们这个充电站,只需要拿着给我们的心跳间隔去发送心跳就可以了。所以,给我们的发送心跳的框架如下:
async def send_heartbeat(self, interval):
request = call.HeartbeatPayload()
while True:
await self.call(request)
await asyncio.sleep(interval)
程序运行的时候,我们可以简要的看一下运行日志截图:
client日志:
server日志:
B02 - Cold Boot Charging Station - Pending(冷启动充电站 - 待定)
其实也可以看出来,此Case属于是B01的衍生Case,用于处理如果云平台回复的并不是Accepted我们应该如何处理。
从时序图中我们也可以清晰的看出,平台给予了我们Pending之后,可能是需要处理一些变量的问题,由于还没有具体规定变量的问题,所以我们先暂时留作以后处理,推荐可以在代码里写TODO,以后进行的时候可以回来再写
所以,暂时对于Pending的一些处理则如下:
if response.status == "Pending":
logging.info("UnConnected to central system, maybe need other operations")
await asyncio.sleep(response.interval)
await self.send_boot_notification(charging_station, reason)
#await self.GetVariables ...
运行结果:
client:
server:
B03 - Cold Boot Charging Station - Rejected(冷启动充电站 - 已拒绝)
本用例描述了平台和充电站的行为,当平台使用拒绝状态通知充电站它(尚未)被接受时。
充电站的首次连接请求被拒绝后,应该在等待一段时间之后再次发送,而且再被接受之前,除了BootNotificationRequest以外不能发送任何其他的信息,所以代码实现也很简单,实现如下:
if response.status == "Rejected":
logging.error(b"UnConnected to central system, reason=='Rejected'")
await self.send_boot_notification(charging_station, reason)
具体现象和B01和B02一样,我就不再测试了。
B04 - Offline Behavior Idle Charging Station(离线行为 空闲充电站)
本用例说明,在通信不可用的情况下,充电站可独立运行。在这种情况下,充电站被称为离线充电站。
所以,在这种情况下,我们要考虑的事情就很多了,比如我们要考虑网络连接断开,或者平台拒绝网络连接,我们需要尝试过一段时间去重新连接,具体最大间隔时间在配置字段里面有写,字段为:OfflineThreshold,但是若没有配置,默认我定义为10秒进行一次重新连接尝试,若有配置,要先获取配置中的值。
关于配置的问题,我打算使用json文件的形式进行存储和编写。比如我将所有需要配置的和默认的项目全都写到JSON文件中,需要读取或者更改都可以通过JSON的形式进行处理。
首先是预留了一个加载JSON文件的一个方法,如果读取到文件了,就将读取到的返回,否则创建这个文件。如果文件为空,也返回默认配置。代码如下:
def load_config(filename):
default_config = {
"charging_station": {"model": "Wallbox XYZ", "vendorName": "anewone"},
"reason": "PowerUp"
}
if not os.path.exists(filename):
with open(filename, "w") as file:
json.dump(default_config, file, indent=4)
return default_config
else:
with open(filename, "r") as file:
try:
return json.load(file)
except json.JSONDecodeError:
return default_config
还预留了一个往JSON文件中写配置的方法,代码如下:
def write_config(filename, config):
with open(filename, "w") as file:
json.dump(config, file, indent=4)
还有一个更新配置的方法:
def update_config(field, config, Upfield, filename):
if field in config:
config[field] = Upfield
logging.info(config)
write_config(filename, config)
言归正传,要考虑到离线断开后重连的情况,那我就需要进行异常处理操作,经过思考和调试以及编写,现在的main函数如下:
async def main():
retryCnt = 0
config = load_config("./config.json")
while True:
try:
async with websockets.connect(
"ws://localhost:9000/CP_1", subprotocols=["ocpp2.0.1"]
) as ws:
charge_point = ChargePoint("CP_1", ws)
charging_station = {"model": "Wallbox XYZ", "vendorName": "anewone"}
reason = "PowerUp"
await asyncio.gather(
charge_point.start(), charge_point.send_boot_notification(charging_station, reason)
)
retryCnt = 0
break
except ConnectionClosed:
retryCnt += 1
OfflineThreshold = config.get("OfflineThreshold", 10)
logging.error(f"Connection lost, trying to reconnect in {OfflineThreshold} seconds...")
await asyncio.sleep(OfflineThreshold)
except ConnectionRefusedError:
retryCnt += 1
OfflineThreshold = config.get("OfflineThreshold", 10)
logging.error(f"Connection refused by the server, trying to reconnect in {OfflineThreshold} seconds...")
await asyncio.sleep(OfflineThreshold)
except asyncio.TimeoutError:
retryCnt += 1
OfflineThreshold = config.get("OfflineThreshold", 10)
logging.error("Connection timed out. Trying again...")
await asyncio.sleep(OfflineThreshold)
except asyncio.CancelledError:
retryCnt += 1
OfflineThreshold = config.get("OfflineThreshold", 10)
logging.error("Connection cancelled. Trying again...")
await asyncio.sleep(OfflineThreshold)
前面其实还是简单的连接处理,主要是后面对于异常情况的一个处理,我会读取文件中的配置,如果配置为空则默认为10秒,否则按照配置文件中的配置进行处理。
如果恢复连接,则重新发送notification以及心跳。
程序如果重新启动应该修改json文件中的内容,而且第二次启动并且连接的时候和第一次应该有不同的reason
所以修改后的现在的代码如下:
except ConnectionClosed:
retryCnt += 1
OfflineThreshold = config.get("OfflineThreshold", 10)
logging.error(f"Connection lost, trying to reconnect in {OfflineThreshold} seconds...")
update_config("reason", config, "Watchdog", "./config.json")
await asyncio.sleep(OfflineThreshold)
except ConnectionRefusedError:
retryCnt += 1
OfflineThreshold = config.get("OfflineThreshold", 10)
logging.error(f"Connection refused by the server, trying to reconnect in {OfflineThreshold} seconds...")
update_config("reason", config, "Watchdog", "./config.json")
await asyncio.sleep(OfflineThreshold)
except asyncio.TimeoutError:
retryCnt += 1
OfflineThreshold = config.get("OfflineThreshold", 10)
logging.error("Connection timed out. Trying again...")
update_config("reason", config, "Watchdog", "./config.json")
await asyncio.sleep(OfflineThreshold)
其实还要判断是不是第一次冷启动系统,这里我采用标志文件的方式。如果读取到了系统中的json文件,则不是第一次冷启动,如果没读取到,则为第一次冷启动。
B05 - Set Variables(设置变量)
充电站可以有很多变量,可以由 CSMS 进行配置/更改。例如,CSMS 可以使用这些变量来影响充电站的行为。本用例描述 CSMS 如何请求充电站设置组件的变量值。CSMS 每次可请求设置多个值。
实现方式暂时来说也比较轻松简单,因为可以采用基本的数据处理方式,同时要参考的是传过来的Value中的Payload是否符合你的预期,而且要在收到后回复相应的Response,所以我打算再创建一个新的JSON文件去存放变量的配置
set_variable_config.json
由于主要我完成的是客户端的内容,并不专注于服务端,所以我将服务端的代码简化了很多,主要进行的是客户端代码的编写,进行了一些简单的逻辑处理以及数据处理,后续根据需求继续细化。现在主要想完成的也就是根据这个case的框架而已,本文也不会完整的将此项目全部写完,因为后续部分属于不可开源的部分
此部分代码中服务端我简单利用心跳来进行调用,客户端这边采用了服务端一样的websocket处理方式,下面是服务端调用代码:
async def on_SetVariables(self):
logging.info("setVariales")
request = call.SetVariablesPayload(
set_variable_data=[{'attributeType': "Actual",
'attributeValue': "cnm",
'component': {
"name": "abc",
"instance": "cnmcnmcnnc",
"evse": {
"id": 1,
"connectorId": 1
}
}, 'variable': {
"name": "ContractValidationOffline"
}}
]
)
response = await self.call(request)
logging.info(f"Received response for SetVariables request: {response}")
服务端此函数调用的逻辑如下:
@on("Heartbeat")
async def on_heartbeat(self):
print("Got a Heartbeat!")
await self.on_SetVariables()
return call_result.HeartbeatPayload(
current_time=datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S") + "Z"
)
客户端处理如下:
@on("SetVariables")
def SetVariables(self, set_variable_data):
for Value in set_variable_data:
logging.debug(Value)
write_json("./set_variable_config.json", set_variable_data)
if load_config("./set_variable_config.json") == set_variable_data:
return call_result.SetVariablesPayload(
set_variable_result=[
{"attribute_status": "Accepted",
"component": {"name": (set_variable_data[0].get("component")).get("name")},
"variable": {"name": "ContractValidationOffline"}} for _ in set_variable_data]
)
elif (set_variable_data[0].get("component")).get("name") is None:
logging.error("Unknow Component Name!")
return call_result.SetVariablesPayload(
set_variable_result=[
{"attribute_status": "UnknownComponent",
"component": {"name": ""},
"variable": {"name": "ContractValidationOffline"}} for _ in set_variable_data]
)
elif (set_variable_data[0].get("variable")).get("name") is None:
logging.error("Unknow variable Name!")
return call_result.SetVariablesPayload(
set_variable_result=[
{"attribute_status": "UnknownVariable",
"component": {"name": (set_variable_data[0].get("component")).get("name")},
"variable": {"name": ""}} for _ in set_variable_data]
)
注意,这部分代码创建在 ChargePoint类之中
实现后效果如下(创建的JSON):
B06 - Get Variables(获取变量)
此Case是专门用来进行平台向充电站获取一些配置变量所进行的事件流
实现方式其实和B05是差不多的,主要是将充电站作为一个Websocket的服务端接受平台的一个请求(getVariablesRequest),得知平台需要得到哪些变量之后,通过相应的Response来进行反馈,所以还是一样的方法进行操作,直接上实现:
后续继续写。。