juham-automation 0.0.31__py3-none-any.whl → 0.0.34__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -14,7 +14,7 @@ class Consumer:
14
14
 
15
15
  """
16
16
 
17
- def __init__(self, name: str, power) -> None:
17
+ def __init__(self, name: str, power: float, weight: float) -> None:
18
18
  """Initialize the consumer
19
19
 
20
20
  Args:
@@ -25,6 +25,7 @@ class Consumer:
25
25
  self.power: float = power
26
26
  self.start: float = 0.0
27
27
  self.stop: float = 0.0
28
+ self.weight: float = weight
28
29
 
29
30
 
30
31
  class EnergyBalancer(Juham):
@@ -102,8 +103,10 @@ class EnergyBalancer(Juham):
102
103
  m (dict[str, Any]): power consumer message
103
104
  ts (float): current time
104
105
  """
105
- self.consumers[m["Unit"]] = Consumer(m["Unit"], m["Power"])
106
- self.info(f"Consumer {m['Unit']} added, power: {m['Power']}")
106
+ self.consumers[m["Unit"]] = Consumer(m["Unit"], m["Power"], m["Weight"])
107
+ self.info(
108
+ f"Consumer {m['Unit']} added, power: {m['Power']}, weight: {m['Weight']}"
109
+ )
107
110
 
108
111
  def update_energy_balance(self, power: float, ts: float) -> None:
109
112
  """Update the current net net energy balance. The change in the balance is calculate the
@@ -164,8 +167,16 @@ class EnergyBalancer(Juham):
164
167
  remaining_ts_consumer: float = (
165
168
  self.energy_balancing_interval - interval_ts
166
169
  ) / num_consumers
170
+ total_weight: float = 0.0
167
171
  for consumer in self.consumers.values():
168
- required_power += consumer.power * remaining_ts_consumer
172
+ total_weight += consumer.weight
173
+ equal_weight: float = total_weight / num_consumers
174
+ for consumer in self.consumers.values():
175
+ required_power += (
176
+ (consumer.weight / equal_weight)
177
+ * consumer.power
178
+ * remaining_ts_consumer
179
+ )
169
180
  return required_power
170
181
 
171
182
  def initialize_consumer_timelines(self, ts: float) -> None:
@@ -185,9 +196,16 @@ class EnergyBalancer(Juham):
185
196
  self.energy_balancing_interval - interval_ts
186
197
  ) / num_consumers
187
198
 
199
+ total_weight: float = 0.0
200
+ for consumer in self.consumers.values():
201
+ total_weight += consumer.weight
202
+ equal_weight: float = total_weight / num_consumers
203
+
188
204
  for consumer in self.consumers.values():
189
205
  consumer.start = interval_ts
190
- consumer.stop = interval_ts + secs_per_consumer
206
+ consumer.stop = (
207
+ interval_ts + secs_per_consumer * consumer.weight / equal_weight
208
+ )
191
209
  interval_ts += secs_per_consumer
192
210
 
193
211
  def publish_energybalance(self, ts: float) -> None:
@@ -209,18 +227,18 @@ class EnergyBalancer(Juham):
209
227
  }
210
228
  self.publish(self.topic_out_diagnostics, json.dumps(m))
211
229
 
212
-
213
230
  # publish consumer statuses to control consumers
214
231
  num_consumers: int = len(self.consumers)
215
232
  if num_consumers == 0:
216
233
  return # If there are no consumers, we simply do nothing
217
234
  interval_ts = ts % self.energy_balancing_interval
218
235
  for consumer in self.consumers.values():
219
- m: dict[str, Any] = {
236
+ m = {
220
237
  "EnergyBalancer": self.name,
221
238
  "Unit": consumer.name,
222
239
  "Power": consumer.power,
223
240
  "Mode": consumer.start <= interval_ts < consumer.stop,
241
+ "Weight": consumer.weight,
224
242
  "Timestamp": ts,
225
243
  }
226
244
  self.publish(self.topic_out_status, json.dumps(m))
@@ -263,4 +281,3 @@ class EnergyBalancer(Juham):
263
281
  self.net_energy_balancing_mode = False
264
282
  self.info("Balance used, or the end of the interval reached, disable")
265
283
  self.net_energy_balance = 0.0 # Reset the energy balance at the interval's end
266
-
@@ -1,3 +1,4 @@
1
+ from datetime import datetime
1
2
  import json
2
3
  from typing import Any
3
4
  from typing_extensions import override
@@ -16,31 +17,66 @@ from juham_core.timeutils import (
16
17
 
17
18
  class HeatingOptimizer(Juham):
18
19
  """Automation class for optimized control of temperature driven home energy consumers e.g hot
19
- water radiators. Reads spot prices, electricity forecast, power meter and temperatures to minimize electricity bill.
20
+ water radiators. Reads spot prices, solar electricity forecast, power meter and
21
+ temperature to minimize electricity bill.
20
22
 
21
23
  Represents a heating system that knows the power rating of its radiator (e.g., 3kW).
24
+ Any number of heating devices can be controlled, each with its own temperature, schedule and electricity price ratings
25
+
22
26
  The system subscribes to the 'power' topic to track the current power balance. If the solar panels
23
27
  generate more energy than is being consumed, the optimizer activates a relay to ensure that all excess energy
24
- produced within that hour is used for heating. The goal is to achieve a net zero energy balance for each hour,
28
+ produced within that balancing interval is used for heating. The goal is to achieve a net zero energy balance for each hour,
25
29
  ensuring that any surplus energy from the solar panels is fully utilized.
26
30
 
27
31
  Computes also UOI - optimization utilization index for each hour, based on the spot price and the solar power forecast.
28
32
  For negative energy balance this determines when energy is consumed. Value of 0 means the hour is expensive, value of 1 means
29
33
  the hour is free. The UOI threshold determines the slots that are allowed to be consumed.
30
-
31
34
  """
32
35
 
33
- maximum_boiler_temperature: float = 70
34
- minimum_boiler_temperature: float = 40
35
36
  energy_balancing_interval: float = 3600
36
- radiator_power: float = 6000 #
37
- operation_threshold: float = 5 * 60
37
+ """Energy balancing interval, as regulated by the industry/converment. In seconds"""
38
+
39
+ radiator_power: float = 6000 # W
40
+ """Radiator power in Watts. This is the maximum power that the radiator can consume."""
41
+
38
42
  heating_hours_per_day: float = 4
43
+ """ Number of hours per day the radiator is allowed to heat."""
44
+
39
45
  schedule_start_hour: float = 0
46
+ """Start hour of the heating schedule."""
47
+
40
48
  schedule_stop_hour: float = 0
49
+ """Stop hour of the heating schedule. Heating is allowed only between these two hours."""
50
+
41
51
  timezone: str = "Europe/Helsinki"
52
+ """ Timezone of the heating system. This is used to convert UTC timestamps to local time."""
53
+
42
54
  expected_average_price: float = 0.2
55
+ """Expected average price of electricity, beyond which the heating is avoided."""
56
+
43
57
  uoi_threshold: float = 0.8
58
+ """Utilization Optimization Index threshold. This is the minimum UOI value that is allowed for the heating to be activated."""
59
+
60
+ balancing_weight: float = 1.0
61
+ """Weight determining how large a share of the time slot a consumer receives compared to others ."""
62
+
63
+ temperature_limits: dict[int, tuple[float, float]] = {
64
+ 1: (20.0, 60.0), # January
65
+ 2: (20.0, 60.0), # February
66
+ 3: (20.0, 60.0), # March
67
+ 4: (20.0, 50.0), # April
68
+ 5: (20.0, 40.0), # May
69
+ 6: (20.0, 22.0), # June
70
+ 7: (20.0, 22.0), # July
71
+ 8: (20.0, 22.0), # August
72
+ 9: (20.0, 50.0), # September
73
+ 10: (20.0, 60.0), # October
74
+ 11: (20.0, 60.0), # November
75
+ 12: (20.0, 60.0), # December
76
+ }
77
+ """Temperature limits for each month. The minimum temperature is maintained regardless of the cost.
78
+ The limits are defined as a dictionary where the keys are month numbers (1-12)
79
+ and the values are tuples of (min_temp, max_temp). The min_temp and max_temp values are in degrees Celsius."""
44
80
 
45
81
  def __init__(
46
82
  self,
@@ -84,9 +120,9 @@ class HeatingOptimizer(Juham):
84
120
  self.start_hour = start_hour
85
121
  self.spot_limit = spot_limit
86
122
 
87
- self.topic_spot = self.make_topic_name("spot")
88
- self.topic_forecast = self.make_topic_name("forecast")
89
- self.topic_temperature = self.make_topic_name(temperature_sensor)
123
+ self.topic_in_spot = self.make_topic_name("spot")
124
+ self.topic_in_forecast = self.make_topic_name("forecast")
125
+ self.topic_in_temperature = self.make_topic_name(temperature_sensor)
90
126
  self.topic_powerplan = self.make_topic_name("powerplan")
91
127
  self.topic_in_energybalance = self.make_topic_name("energybalance/status")
92
128
  self.topic_out_energybalance = self.make_topic_name("energybalance/consumers")
@@ -107,9 +143,9 @@ class HeatingOptimizer(Juham):
107
143
  def on_connect(self, client: object, userdata: Any, flags: int, rc: int) -> None:
108
144
  super().on_connect(client, userdata, flags, rc)
109
145
  if rc == 0:
110
- self.subscribe(self.topic_spot)
111
- self.subscribe(self.topic_forecast)
112
- self.subscribe(self.topic_temperature)
146
+ self.subscribe(self.topic_in_spot)
147
+ self.subscribe(self.topic_in_forecast)
148
+ self.subscribe(self.topic_in_temperature)
113
149
  self.subscribe(self.topic_in_energybalance)
114
150
  self.register_as_consumer()
115
151
 
@@ -120,9 +156,25 @@ class HeatingOptimizer(Juham):
120
156
  consumer: dict[str, Any] = {
121
157
  "Unit": self.name,
122
158
  "Power": self.radiator_power,
159
+ "Weight": self.balancing_weight,
123
160
  }
124
161
  self.publish(self.topic_out_energybalance, json.dumps(consumer), 1, False)
125
- self.info(f"Registered {self.name} as consumer with {self.radiator_power}W", "")
162
+ self.info(
163
+ f"Registered {self.name} as consumer with {self.radiator_power}W power",
164
+ "",
165
+ )
166
+
167
+ # Function to get the temperature limits based on the current month
168
+ def get_temperature_limits_for_current_month(self) -> tuple[float, float]:
169
+ """Get the temperature limits for the current month.
170
+ The limits are defined in a dictionary where the keys are month numbers (1-12)
171
+ and the values are tuples of (min_temp, max_temp).
172
+ Returns: tuple: (min_temp, max_temp)
173
+ """
174
+ current_month: int = datetime.now().month
175
+ # Get the min and max temperatures for the current month
176
+ min_temp, max_temp = self.temperature_limits[current_month]
177
+ return min_temp, max_temp
126
178
 
127
179
  def sort_by_rank(
128
180
  self, hours: list[dict[str, Any]], ts_utc_now: float
@@ -170,7 +222,7 @@ class HeatingOptimizer(Juham):
170
222
  reverse=True,
171
223
  )
172
224
  self.debug(
173
- f"Sorted {len(sh)} days of forecast starting at {timestampstr(ts_utc)}"
225
+ f"{self.name} sorted {len(sh)} days of forecast starting at {timestampstr(ts_utc)}"
174
226
  )
175
227
  ranked_hours: list[dict[str, Any]] = []
176
228
 
@@ -178,7 +230,9 @@ class HeatingOptimizer(Juham):
178
230
  utc_ts: float = float(h["ts"])
179
231
  if utc_ts >= ts_utc:
180
232
  ranked_hours.append(h)
181
- self.debug(f"Forecast sorted for the next {str(len(ranked_hours))} hours")
233
+ self.debug(
234
+ f"{self.name} forecast sorted for the next {str(len(ranked_hours))} hours"
235
+ )
182
236
  return ranked_hours
183
237
 
184
238
  def on_spot(self, m: list[dict[str, Any]], ts_quantized: float) -> None:
@@ -206,7 +260,7 @@ class HeatingOptimizer(Juham):
206
260
 
207
261
  self.ranked_solarpower = self.sort_by_power(forecast, ts_utc_quantized)
208
262
  self.debug(
209
- f"Solar energy forecast received and ranked for {len(self.ranked_solarpower)} hours"
263
+ f"{self.name} solar energy forecast received and ranked for {len(self.ranked_solarpower)} hours"
210
264
  )
211
265
  self.power_plan = [] # reset power plan, it depends on forecast
212
266
 
@@ -215,13 +269,13 @@ class HeatingOptimizer(Juham):
215
269
  m = None
216
270
  ts: float = timestamp()
217
271
  ts_utc_quantized: float = quantize(3600, ts - 3600)
218
- if msg.topic == self.topic_spot:
272
+ if msg.topic == self.topic_in_spot:
219
273
  self.on_spot(json.loads(msg.payload.decode()), ts_utc_quantized)
220
274
  return
221
- elif msg.topic == self.topic_forecast:
275
+ elif msg.topic == self.topic_in_forecast:
222
276
  self.on_forecast(json.loads(msg.payload.decode()), ts_utc_quantized)
223
277
  return
224
- elif msg.topic == self.topic_temperature:
278
+ elif msg.topic == self.topic_in_temperature:
225
279
  m = json.loads(msg.payload.decode())
226
280
  self.current_temperature = m["temperature"]
227
281
  elif msg.topic == self.topic_in_energybalance:
@@ -250,24 +304,24 @@ class HeatingOptimizer(Juham):
250
304
  self.relay_started_ts = ts_utc_now
251
305
 
252
306
  if not self.ranked_spot_prices:
253
- self.debug("Waiting spot prices...", "")
307
+ self.debug(f"{self.name} waiting spot prices...", "")
254
308
  return
255
309
 
256
310
  if not self.power_plan:
257
311
  self.power_plan = self.create_power_plan()
258
312
  self.heating_plan = []
259
313
  self.info(
260
- f"Power plan of length {len(self.power_plan)} created",
314
+ f"{self.name} power plan of length {len(self.power_plan)} created",
261
315
  str(self.power_plan),
262
316
  )
263
317
 
264
318
  if not self.power_plan:
265
- self.error("Failed to create a power plan", "")
319
+ self.error(f"{self.name} failed to create a power plan", "")
266
320
  return
267
321
 
268
322
  if len(self.power_plan) < 3:
269
323
  self.warning(
270
- f"Suspiciously short {len(self.power_plan)} power plan, wait more data ..",
324
+ f"{self.name} has suspiciously short {len(self.power_plan)} power plan, waiting for more data ..",
271
325
  "",
272
326
  )
273
327
  self.heating_plan = []
@@ -276,21 +330,25 @@ class HeatingOptimizer(Juham):
276
330
 
277
331
  if not self.ranked_solarpower or len(self.ranked_solarpower) < 4:
278
332
  self.warning(
279
- f"Short of forecast {len(self.ranked_solarpower)}, optimization compromised..",
333
+ f"{self.name} short of forecast {len(self.ranked_solarpower)}, optimization compromised..",
280
334
  "",
281
335
  )
282
336
 
283
337
  if not self.heating_plan:
284
338
  self.heating_plan = self.create_heating_plan()
285
339
  if not self.heating_plan:
286
- self.error("Failed to create heating plan")
340
+ self.error(f"{self.name} failed to create heating plan")
287
341
  return
288
342
  else:
289
343
  self.info(
290
- f"Heating plan of length {len(self.heating_plan)} created", ""
344
+ f"{self.name} heating plan of length {len(self.heating_plan)} created",
345
+ "",
291
346
  )
292
347
  if len(self.heating_plan) < 3:
293
- self.info(f"Short heating plan {len(self.heating_plan)}, no can do", "")
348
+ self.warning(
349
+ f"{self.name} has too short heating plan {len(self.heating_plan)}, no can do",
350
+ "",
351
+ )
294
352
  self.heating_plan = []
295
353
  self.power_plan = []
296
354
  return
@@ -304,7 +362,7 @@ class HeatingOptimizer(Juham):
304
362
  }
305
363
  self.publish(self.topic_out_power, json.dumps(heat), 1, False)
306
364
  self.info(
307
- f"Relay state {self.name} changed to {relay} at {timestampstr(ts_utc_now)}",
365
+ f"{self.name} relay changed to {relay} at {timestampstr(ts_utc_now)}",
308
366
  "",
309
367
  )
310
368
  self.current_relay_state = relay
@@ -322,7 +380,6 @@ class HeatingOptimizer(Juham):
322
380
  if m["Unit"] == self.name:
323
381
  self.net_energy_balance_mode = m["Mode"]
324
382
 
325
-
326
383
  def consider_heating(self, ts: float) -> int:
327
384
  """Consider whether the target boiler needs heating. Check first if the solar
328
385
  energy is enough to heat the water the remaining time in the current slot.
@@ -337,28 +394,45 @@ class HeatingOptimizer(Juham):
337
394
 
338
395
  # check if we have excess energy to spent within the current slot
339
396
  if self.net_energy_balance_mode:
340
- self.info("Positive net energy balance, spend it for heating")
397
+ self.debug(
398
+ f"{self.name} with positive net energy balance, spend it for heating"
399
+ )
341
400
  return 1
342
401
 
343
- # no free energy available, don't spend if the current temperature is already high enough
344
- if self.current_temperature > self.maximum_boiler_temperature:
345
- self.info(
346
- f"Current temperature {self.current_temperature}C already beyond max {self.maximum_boiler_temperature}C"
347
- )
348
- return 0
349
402
  hour = timestamp_hour(ts)
403
+ state: int = -1
350
404
 
351
405
  # check if we are within the heating plan and see what the plan says
352
406
  for pp in self.heating_plan:
353
407
  ppts: float = pp["Timestamp"]
354
408
  h: float = timestamp_hour(ppts)
355
409
  if h == hour:
356
- return pp["State"]
410
+ state = pp["State"]
411
+ break
357
412
 
358
- # if we are not within the heating plan, then we are not heating
359
- # this should not happen, but just in case
360
- self.error(f"Cannot find heating plan for hour {hour}")
361
- return 0
413
+ if state == -1:
414
+ self.error(f"{self.name} cannot find heating plan for hour {hour}")
415
+ return 0
416
+
417
+ min_temp, max_temp = self.get_temperature_limits_for_current_month()
418
+ self.debug(
419
+ f"{self.name} month's temperature limits: Min = {min_temp}°C, Max = {max_temp}°C"
420
+ )
421
+
422
+ # don't heat if the current temperature is already high enough
423
+ if self.current_temperature > max_temp:
424
+ self.debug(
425
+ f"{self.name} plan {state}, temp {self.current_temperature}°C already beyond max {max_temp}°C"
426
+ )
427
+ return 0
428
+ # heat if the current temperature is below the required minimum
429
+ if self.current_temperature < min_temp:
430
+ self.debug(
431
+ f"{self.name} plan {state}, temp {self.current_temperature}°C below min {min_temp}°C"
432
+ )
433
+ return 1
434
+
435
+ return state # 1 = heating, 0 = not heating
362
436
 
363
437
  # compute utilization optimization index
364
438
  def compute_uoi(
@@ -425,7 +499,7 @@ class HeatingOptimizer(Juham):
425
499
  ts_utc_quantized = quantize(3600, timestamp() - 3600)
426
500
  starts: str = timestampstr(ts_utc_quantized)
427
501
  self.info(
428
- f"Trying to create power plan starting at {starts} with {len(self.ranked_spot_prices)} hourly spot prices",
502
+ f"{self.name} created power plan starting at {starts} with {len(self.ranked_spot_prices)} hours of spot prices",
429
503
  "",
430
504
  )
431
505
 
@@ -438,10 +512,11 @@ class HeatingOptimizer(Juham):
438
512
  )
439
513
 
440
514
  if len(spots) == 0:
441
- self.debug(
515
+ self.info(
442
516
  f"No spot prices initialized yet, can't proceed",
443
517
  "",
444
518
  )
519
+ return []
445
520
  self.info(
446
521
  f"Have spot prices for the next {len(spots)} hours",
447
522
  "",
@@ -497,7 +572,7 @@ class HeatingOptimizer(Juham):
497
572
 
498
573
  shplan = sorted(hplan, key=lambda x: x["FOM"], reverse=True)
499
574
 
500
- self.debug(f"Powerplan starts {starts} up to {len(shplan)} hours")
575
+ self.debug(f"{self.name} powerplan starts {starts} up to {len(shplan)} hours")
501
576
  return shplan
502
577
 
503
578
  def enable_relay(
@@ -547,5 +622,5 @@ class HeatingOptimizer(Juham):
547
622
  heating_plan.append(heat)
548
623
  hour = hour + 1
549
624
 
550
- self.info(f"Heating plan of {len(heating_plan)} hours created", "")
625
+ self.info(f"{self.name} heating plan of {len(heating_plan)} hours created", "")
551
626
  return heating_plan
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: juham-automation
3
- Version: 0.0.31
3
+ Version: 0.0.34
4
4
  Summary: Juha's Ultimate Home Automation Masterpiece
5
5
  Author-email: J Meskanen <juham.api@gmail.com>
6
6
  Maintainer-email: "J. Meskanen" <juham.api@gmail.com>
@@ -2,9 +2,9 @@ juham_automation/__init__.py,sha256=32BL36bhT7OaSw22H7st-7-3IXcFM2Pf5js80hNA8W0,
2
2
  juham_automation/japp.py,sha256=L2u1mfKvun2fiXhB3AEJD9zMDcdFZ3_doXZYJJzu9tg,1646
3
3
  juham_automation/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
4
4
  juham_automation/automation/__init__.py,sha256=uxkIrcRSp1cFikn-oBRtQ8XiT9cSf7xjm3CS1RN7lAQ,522
5
- juham_automation/automation/energybalancer.py,sha256=_08KikTDO3zHsRCV0oI7xyRpMyk594Q1C0acalRQoQs,10855
5
+ juham_automation/automation/energybalancer.py,sha256=VcoDYtQbZC54Fv6u0EfM6DcPYupd9nlDT0Znn3-6ml4,11533
6
6
  juham_automation/automation/energycostcalculator.py,sha256=v30wxRpuY2gGBSMJifrFRTjsRU9t-iCiq33Vds7s3O8,10877
7
- juham_automation/automation/heatingoptimizer.py,sha256=LMZ4Cr1CTsk1HI9D0P27l1bPFDepylhDh87dTd204r0,21367
7
+ juham_automation/automation/heatingoptimizer.py,sha256=X7BY40NoCes3aRz5tKRuEHMRi3TVtcxkgIYffp3_k14,24601
8
8
  juham_automation/automation/powermeter_simulator.py,sha256=3WZcjByRTdqnC77l7LjP-TEjmZ8XBEO4hClYsrjxmBE,4549
9
9
  juham_automation/automation/spothintafi.py,sha256=cZbi7w2fVweHX_fh1r5MTjGdesX9wDQta2mfVjtiwvw,4331
10
10
  juham_automation/automation/watercirculator.py,sha256=a8meMNaONbHcIH3y0vP0UulJc1-gZiLZpw7H8kAOreY,6410
@@ -17,9 +17,9 @@ juham_automation/ts/log_ts.py,sha256=XsNaazuPmRUZLUqxU0DZae_frtT6kAFcXJTc598CtOA
17
17
  juham_automation/ts/power_ts.py,sha256=e7bSeZjitY4C_gLup9L0NjvU_WnQsl3ayDhVShj32KY,1399
18
18
  juham_automation/ts/powermeter_ts.py,sha256=gXzfK2S4SzrQ9GqM0tsLaV6z_vYmTkBatTcaivASSXs,2188
19
19
  juham_automation/ts/powerplan_ts.py,sha256=LZeE7TnzPCDaugggKlaV-K48lDwwnC1ZNum50JYAWaY,1482
20
- juham_automation-0.0.31.dist-info/licenses/LICENSE.rst,sha256=QVHD5V5_HSys2PdPdig_xKggDj8cGX33ALKqRsYyjtI,1089
21
- juham_automation-0.0.31.dist-info/METADATA,sha256=6PmgwoKzNtP2ZHHr_L_fhCcavtvkCOfL4COQcCje2A8,6837
22
- juham_automation-0.0.31.dist-info/WHEEL,sha256=pxyMxgL8-pra_rKaQ4drOZAegBVuX-G_4nRHjjgWbmo,91
23
- juham_automation-0.0.31.dist-info/entry_points.txt,sha256=h-KzuKjmGPd4_iX_oiGvxx4IEc97dVbGGlhdh5ctbpI,605
24
- juham_automation-0.0.31.dist-info/top_level.txt,sha256=jfohvtocvX_gfT21AhJk7Iay5ZiQsS3HzrDjF7S4Qp0,17
25
- juham_automation-0.0.31.dist-info/RECORD,,
20
+ juham_automation-0.0.34.dist-info/licenses/LICENSE.rst,sha256=QVHD5V5_HSys2PdPdig_xKggDj8cGX33ALKqRsYyjtI,1089
21
+ juham_automation-0.0.34.dist-info/METADATA,sha256=LTcnFtw37Rf299ItV-STdD0g4hCaykQW0Gf7YUMjGg0,6837
22
+ juham_automation-0.0.34.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
23
+ juham_automation-0.0.34.dist-info/entry_points.txt,sha256=h-KzuKjmGPd4_iX_oiGvxx4IEc97dVbGGlhdh5ctbpI,605
24
+ juham_automation-0.0.34.dist-info/top_level.txt,sha256=jfohvtocvX_gfT21AhJk7Iay5ZiQsS3HzrDjF7S4Qp0,17
25
+ juham_automation-0.0.34.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (79.0.0)
2
+ Generator: setuptools (80.9.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5