Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1# SPDX-License-Identifier: Apache-2.0 

2 

3# Copyright 2020 Contributors to OpenLEADR 

4 

5# Licensed under the Apache License, Version 2.0 (the "License"); 

6# you may not use this file except in compliance with the License. 

7# You may obtain a copy of the License at 

8 

9# http://www.apache.org/licenses/LICENSE-2.0 

10 

11# Unless required by applicable law or agreed to in writing, software 

12# distributed under the License is distributed on an "AS IS" BASIS, 

13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 

14# See the License for the specific language governing permissions and 

15# limitations under the License. 

16 

17from dataclasses import dataclass, field, asdict, is_dataclass 

18from typing import List, Dict 

19from datetime import datetime, timezone, timedelta 

20from openleadr import utils 

21from openleadr import enums 

22 

23 

24@dataclass 

25class AggregatedPNode: 

26 node: str 

27 

28 

29@dataclass 

30class EndDeviceAsset: 

31 mrid: str 

32 

33 

34@dataclass 

35class MeterAsset: 

36 mrid: str 

37 

38 

39@dataclass 

40class PNode: 

41 node: str 

42 

43 

44@dataclass 

45class FeatureCollection: 

46 id: str 

47 location: dict 

48 

49 

50@dataclass 

51class ServiceArea: 

52 feature_collection: FeatureCollection 

53 

54 

55@dataclass 

56class ServiceDeliveryPoint: 

57 node: str 

58 

59 

60@dataclass 

61class ServiceLocation: 

62 node: str 

63 

64 

65@dataclass 

66class TransportInterface: 

67 point_of_receipt: str 

68 point_of_delivery: str 

69 

70 

71@dataclass 

72class Target: 

73 aggregated_p_node: AggregatedPNode = None 

74 end_device_asset: EndDeviceAsset = None 

75 meter_asset: MeterAsset = None 

76 p_node: PNode = None 

77 service_area: ServiceArea = None 

78 service_delivery_point: ServiceDeliveryPoint = None 

79 service_location: ServiceLocation = None 

80 transport_interface: TransportInterface = None 

81 group_id: str = None 

82 group_name: str = None 

83 resource_id: str = None 

84 ven_id: str = None 

85 party_id: str = None 

86 

87 def __repr__(self): 

88 targets = {key: value for key, value in asdict(self).items() if value is not None} 

89 targets_str = ", ".join(f"{key}={value}" for key, value in targets.items()) 

90 return f"Target('{targets_str}')" 

91 

92 

93@dataclass 

94class EventDescriptor: 

95 event_id: str 

96 modification_number: int 

97 market_context: str 

98 event_status: str 

99 

100 created_date_time: datetime = None 

101 modification_date_time: datetime = None 

102 priority: int = 0 

103 test_event: bool = False 

104 vtn_comment: str = None 

105 

106 def __post_init__(self): 

107 if self.modification_date_time is None: 

108 self.modification_date_time = datetime.now(timezone.utc) 

109 if self.created_date_time is None: 

110 self.created_date_time = datetime.now(timezone.utc) 

111 if self.modification_number is None: 

112 self.modification_number = 0 

113 

114 

115@dataclass 

116class ActivePeriod: 

117 dtstart: datetime 

118 duration: timedelta 

119 tolerance: dict = None 

120 notification_period: dict = None 

121 ramp_up_period: dict = None 

122 recovery_period: dict = None 

123 

124 

125@dataclass 

126class Interval: 

127 dtstart: datetime 

128 duration: timedelta 

129 signal_payload: float 

130 uid: int = None 

131 

132 

133@dataclass 

134class SamplingRate: 

135 min_period: timedelta = None 

136 max_period: timedelta = None 

137 on_change: bool = False 

138 

139 

140@dataclass 

141class PowerAttributes: 

142 hertz: int = 50 

143 voltage: int = 230 

144 ac: bool = True 

145 

146 

147@dataclass 

148class Measurement: 

149 name: str 

150 description: str 

151 unit: str 

152 acceptable_units: List[str] = field(repr=False, default_factory=list) 

153 scale: str = None 

154 power_attributes: PowerAttributes = None 

155 pulse_factor: int = None 

156 ns: str = 'power' 

157 

158 def __post_init__(self): 

159 if self.name not in enums._MEASUREMENT_NAMESPACES: 

160 self.name = 'customUnit' 

161 self.ns = enums._MEASUREMENT_NAMESPACES[self.name] 

162 

163 

164@dataclass 

165class EventSignal: 

166 intervals: List[Interval] 

167 signal_name: str 

168 signal_type: str 

169 signal_id: str 

170 current_value: float = None 

171 targets: List[Target] = None 

172 targets_by_type: Dict = None 

173 measurement: Measurement = None 

174 

175 def __post_init__(self): 

176 if self.signal_type not in enums.SIGNAL_TYPE.values: 

177 raise ValueError(f"""The signal_type must be one of '{"', '".join(enums.SIGNAL_TYPE.values)}', """ 

178 f"""you specified: '{self.signal_type}'.""") 

179 if self.signal_name not in enums.SIGNAL_NAME.values and not self.signal_name.startswith('x-'): 

180 raise ValueError(f"""The signal_name must be one of '{"', '".join(enums.SIGNAL_TYPE.values)}', """ 

181 f"""or it must begin with 'x-'. You specified: '{self.signal_name}'""") 

182 if self.targets is None and self.targets_by_type is None: 

183 return 

184 elif self.targets_by_type is None: 

185 list_of_targets = [asdict(target) if is_dataclass(target) else target for target in self.targets] 

186 targets_by_type = utils.group_targets_by_type(list_of_targets) 

187 if len(targets_by_type) > 1: 

188 raise ValueError("In OpenADR, the EventSignal target may only be of type endDeviceAsset. " 

189 f"You provided types: '{', '.join(targets_by_type)}'") 

190 elif self.targets is None: 

191 self.targets = [Target(**target) for target in utils.ungroup_targets_by_type(self.targets_by_type)] 

192 elif self.targets is not None and self.targets_by_type is not None: 

193 list_of_targets = [asdict(target) if is_dataclass(target) else target for target in self.targets] 

194 if utils.group_targets_by_type(list_of_targets) != self.targets_by_type: 

195 raise ValueError("You assigned both 'targets' and 'targets_by_type' in your event, " 

196 "but the two were not consistent with each other. " 

197 f"You supplied 'targets' = {self.targets} and " 

198 f"'targets_by_type' = {self.targets_by_type}") 

199 

200 

201@dataclass 

202class Event: 

203 event_descriptor: EventDescriptor 

204 event_signals: List[EventSignal] 

205 targets: List[Target] = None 

206 targets_by_type: Dict = None 

207 active_period: ActivePeriod = None 

208 response_required: str = 'always' 

209 

210 def __post_init__(self): 

211 if self.active_period is None: 

212 dtstart = min([i['dtstart'] 

213 if isinstance(i, dict) else i.dtstart 

214 for s in self.event_signals for i in s.intervals]) 

215 duration = max([i['dtstart'] + i['duration'] 

216 if isinstance(i, dict) else i.dtstart + i.duration 

217 for s in self.event_signals for i in s.intervals]) - dtstart 

218 self.active_period = ActivePeriod(dtstart=dtstart, 

219 duration=duration) 

220 if self.targets is None and self.targets_by_type is None: 

221 raise ValueError("You must supply either 'targets' or 'targets_by_type'.") 

222 elif self.targets_by_type is None: 

223 list_of_targets = [asdict(target) if is_dataclass(target) else target for target in self.targets] 

224 self.targets_by_type = utils.group_targets_by_type(list_of_targets) 

225 elif self.targets is None: 

226 self.targets = [Target(**target) for target in utils.ungroup_targets_by_type(self.targets_by_type)] 

227 elif self.targets is not None and self.targets_by_type is not None: 

228 list_of_targets = [asdict(target) if is_dataclass(target) else target for target in self.targets] 

229 if utils.group_targets_by_type(list_of_targets) != self.targets_by_type: 

230 raise ValueError("You assigned both 'targets' and 'targets_by_type' in your event, " 

231 "but the two were not consistent with each other. " 

232 f"You supplied 'targets' = {self.targets} and " 

233 f"'targets_by_type' = {self.targets_by_type}") 

234 # Set the event status 

235 self.event_descriptor.event_status = utils.determine_event_status(self.active_period) 

236 

237 

238@dataclass 

239class Response: 

240 response_code: int 

241 response_description: str 

242 request_id: str 

243 

244 

245@dataclass 

246class ReportDescription: 

247 r_id: str # Identifies a specific datapoint in a report 

248 market_context: str 

249 reading_type: str 

250 report_subject: Target 

251 report_data_source: Target 

252 report_type: str 

253 sampling_rate: SamplingRate 

254 measurement: Measurement = None 

255 

256 

257@dataclass 

258class ReportPayload: 

259 r_id: str 

260 value: float 

261 confidence: int = None 

262 accuracy: int = None 

263 

264 

265@dataclass 

266class ReportInterval: 

267 dtstart: datetime 

268 report_payload: ReportPayload 

269 duration: timedelta = None 

270 

271 

272@dataclass 

273class Report: 

274 report_specifier_id: str # This is what the VEN calls this report 

275 report_name: str # Usually one of the default ones (enums.REPORT_NAME) 

276 report_request_id: str = None # Usually empty 

277 report_descriptions: List[ReportDescription] = None 

278 created_date_time: datetime = None 

279 

280 dtstart: datetime = None # For delivering values 

281 duration: timedelta = None # For delivering values 

282 intervals: List[ReportInterval] = None # For delivering values 

283 data_collection_mode: str = 'incremental' 

284 

285 def __post_init__(self): 

286 if self.created_date_time is None: 

287 self.created_date_time = datetime.now(timezone.utc) 

288 if self.report_descriptions is None: 

289 self.report_descriptions = [] 

290 

291 

292@dataclass 

293class SpecifierPayload: 

294 r_id: str 

295 reading_type: str 

296 measurement: Measurement = None 

297 

298 

299@dataclass 

300class ReportSpecifier: 

301 report_specifier_id: str # This is what the VEN called this report 

302 granularity: timedelta 

303 specifier_payloads: List[SpecifierPayload] 

304 report_interval: Interval = None 

305 report_back_duration: timedelta = None 

306 

307 

308@dataclass 

309class ReportRequest: 

310 report_request_id: str 

311 report_specifier: ReportSpecifier