juham-automation 0.1.3__py3-none-any.whl → 0.1.4__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.
@@ -72,10 +72,10 @@ class EnergyCostCalculator(Juham):
72
72
  self.error(f"Unknown event {msg.topic}")
73
73
 
74
74
  def on_spot(self, spot: dict[Any, Any]) -> None:
75
- """Stores the received per hour electricity prices to spots list.
75
+ """Stores the received per slot electricity prices to spots list.
76
76
 
77
77
  Args:
78
- spot (list): list of hourly spot prices
78
+ spot (list): list of spot prices
79
79
  """
80
80
 
81
81
  for s in spot:
@@ -93,14 +93,29 @@ class EnergyCostCalculator(Juham):
93
93
  return price * self._joule_to_kwh_coeff
94
94
 
95
95
 
96
-
97
96
  def get_price_at(self, ts: float) -> float:
98
97
  """Return the spot price applicable at the given timestamp.
98
+
99
99
  Args:
100
- ts (float): current time
100
+ ts (float): current time (epoch seconds)
101
+
101
102
  Returns:
102
- Electricity prices for the given interval
103
+ float: PriceWithTax for the slot that contains ts. Returns the last
104
+ known price if ts is equal/after the last spot timestamp.
105
+ Returns 0.0 and logs an error if no matching slot is found.
103
106
  """
107
+ if not self.spots:
108
+ self.error(f"PANIC: no spot prices available; lookup ts={ts}")
109
+ return 0.0
110
+
111
+ # ensure spots sorted by timestamp (defensive)
112
+ try:
113
+ # cheap check — assumes list of dicts with "Timestamp"
114
+ if any(self.spots[i]["Timestamp"] > self.spots[i + 1]["Timestamp"] for i in range(len(self.spots) - 1)):
115
+ self.spots.sort(key=lambda r: r["Timestamp"])
116
+ except Exception:
117
+ # if unexpected structure, still try safe path below and log
118
+ self.debug("get_price_at: spot list structure unexpected while checking sort order", "")
104
119
 
105
120
  for i in range(0, len(self.spots) - 1):
106
121
  r0 = self.spots[i]
@@ -111,10 +126,18 @@ class EnergyCostCalculator(Juham):
111
126
  return r0["PriceWithTax"]
112
127
 
113
128
  # If timestamp is exactly equal to the last spot timestamp or beyond
114
- if ts >= self.spots[-1]["Timestamp"]:
115
- return self.spots[-1]["PriceWithTax"]
116
-
117
- self.error(f"PANIC: Timestamp {ts} out of bounds for spot price lookup")
129
+ last = self.spots[-1]
130
+ if ts >= last["Timestamp"]:
131
+ return last["PriceWithTax"]
132
+
133
+ # If we get here, ts is before the first spot timestamp
134
+ first = self.spots[0]
135
+ self.error(
136
+ f"PANIC: Timestamp {ts} out of bounds for spot price lookup; "
137
+ f"first=(ts={first['Timestamp']}, price={first.get('PriceWithTax')}), "
138
+ f"last=(ts={last['Timestamp']}, price={last.get('PriceWithTax')}), "
139
+ f"len(spots)={len(self.spots)}"
140
+ )
118
141
  return 0.0
119
142
 
120
143
 
@@ -170,12 +193,12 @@ class EnergyCostCalculator(Juham):
170
193
  self.net_energy_balance_cost_hour = 0.0
171
194
  self.net_energy_balance_cost_day = 0.0
172
195
  self.current_ts = ts_now
173
- self.net_energy_balance_start_hour = quantize(
174
- 3600, ts_now
175
- )
176
196
  self.net_energy_balance_start_interval = quantize(
177
197
  self.energy_balancing_interval, ts_now
178
198
  )
199
+ self.net_energy_balance_start_hour = quantize(
200
+ 3600, ts_now
201
+ )
179
202
  else:
180
203
  # calculate cost of energy consumed/produced
181
204
  dp: float = self.calculate_net_energy_cost(self.current_ts, ts_now, power)
@@ -206,7 +229,7 @@ class EnergyCostCalculator(Juham):
206
229
 
207
230
  # Check if the current energy balancing interval has ended
208
231
  # If so, reset the net_energy_balance attribute for the next interval
209
- if ts_now - self.net_energy_balance_start_hour > self.energy_balancing_interval:
232
+ if ts_now - self.net_energy_balance_start_interval > self.energy_balancing_interval:
210
233
  # publish average energy cost per hour
211
234
  if abs(self.total_balance_interval) > 0:
212
235
  msg = {
@@ -8,13 +8,12 @@ from juham_core import Juham
8
8
  from juham_core.timeutils import (
9
9
  quantize,
10
10
  timestamp,
11
- timestamp_hour,
12
11
  timestampstr,
13
- is_hour_within_schedule,
14
- timestamp_hour_local,
15
12
  )
16
13
 
17
14
 
15
+
16
+
18
17
  class HeatingOptimizer(Juham):
19
18
  """Automation class for optimized control of temperature driven home energy consumers e.g hot
20
19
  water radiators. Reads spot prices, solar electricity forecast, power meter and
@@ -39,14 +38,14 @@ class HeatingOptimizer(Juham):
39
38
  radiator_power: float = 6000 # W
40
39
  """Radiator power in Watts. This is the maximum power that the radiator can consume."""
41
40
 
42
- heating_hours_per_day: float = 4
43
- """ Number of hours per day the radiator is allowed to heat."""
41
+ heating_slots_per_day: float = 4
42
+ """ Number of slots per day the radiator is allowed to heat."""
44
43
 
45
- schedule_start_hour: float = 0
46
- """Start hour of the heating schedule."""
44
+ schedule_start_slot: float = 0
45
+ """Start slots of the heating schedule."""
47
46
 
48
- schedule_stop_hour: float = 0
49
- """Stop hour of the heating schedule. Heating is allowed only between these two hours."""
47
+ schedule_stop_slot: float = 0
48
+ """Stop slot of the heating schedule. Heating is allowed only between start-stop slots."""
50
49
 
51
50
  timezone: str = "Europe/Helsinki"
52
51
  """ Timezone of the heating system. This is used to convert UTC timestamps to local time."""
@@ -116,8 +115,8 @@ class HeatingOptimizer(Juham):
116
115
  """
117
116
  super().__init__(name)
118
117
 
119
- self.heating_hours_per_day = num_hours
120
- self.start_hour = start_hour
118
+ self.heating_slots_per_day = num_hours * ( 3600 / self.energy_balancing_interval)
119
+ self.start_slot = start_hour * (3600 / self.energy_balancing_interval)
121
120
  self.spot_limit = spot_limit
122
121
 
123
122
  self.topic_in_spot = self.make_topic_name("spot")
@@ -130,10 +129,10 @@ class HeatingOptimizer(Juham):
130
129
 
131
130
  self.current_temperature : float = 100.0
132
131
  self.current_relay_state : int = -1
133
- self.heating_plan: list[dict[str, int]] = []
134
- self.power_plan: list[dict[str, Any]] = []
135
- self.ranked_spot_prices: list[dict[Any, Any]] = []
136
- self.ranked_solarpower: list[dict[Any, Any]] = []
132
+ self.heating_plan: list[dict[str, int]] = [] # in slots
133
+ self.power_plan: list[dict[str, Any]] = [] # in slots
134
+ self.ranked_spot_prices: list[dict[Any, Any]] = [] # in slots
135
+ self.ranked_solarpower: list[dict[Any, Any]] = [] # in hours
137
136
  self.relay: bool = False
138
137
  self.relay_started_ts: float = 0
139
138
  self.net_energy_balance_mode: bool = False
@@ -148,6 +147,43 @@ class HeatingOptimizer(Juham):
148
147
  self.subscribe(self.topic_in_energybalance)
149
148
  self.register_as_consumer()
150
149
 
150
+ def is_slot_within_schedule(self, slot: int, start_slot: int, stop_slot: int) -> bool:
151
+ """Check if the given slot is within the schedule.
152
+
153
+ Args:
154
+ slot (int): slot to check
155
+ start_slot (int): start slot of the schedule
156
+ stop_slot (int): stop slot of the schedule
157
+ Returns:
158
+ bool: true if the slot is within the schedule
159
+ """
160
+ if start_slot < stop_slot:
161
+ return slot >= start_slot and slot < stop_slot
162
+ else:
163
+ return slot >= start_slot or slot < stop_slot
164
+
165
+
166
+ def slots_per_day(self) -> int:
167
+ return int(24 * 3600 / self.energy_balancing_interval)
168
+
169
+
170
+ def timestamp_slot(self, ts: float) -> int:
171
+ """Get the time slot for the given timestamp and interval.
172
+
173
+ Args:
174
+ ts (float): timestamp
175
+ interval (float): interval in seconds
176
+
177
+ Returns:
178
+ float: time slot
179
+ """
180
+ dt = datetime.utcfromtimestamp(ts)
181
+ total_seconds = dt.hour * 3600 + dt.minute * 60 + dt.second
182
+ slot : int = total_seconds // self.energy_balancing_interval
183
+ return slot
184
+
185
+
186
+
151
187
  def register_as_consumer(self) -> None:
152
188
  """Register this device as a consumer to the energy balancer. The energy balancer will then add this device
153
189
  to its list of consumers and will tell the device when to heat."""
@@ -176,7 +212,7 @@ class HeatingOptimizer(Juham):
176
212
  return min_temp, max_temp
177
213
 
178
214
  def sort_by_rank(
179
- self, hours: list[dict[str, Any]], ts_utc_now: float
215
+ self, slot: list[dict[str, Any]], ts_utc_now: float
180
216
  ) -> list[dict[str, Any]]:
181
217
  """Sort the given electricity prices by their rank value. Given a list
182
218
  of electricity prices, return a sorted list from the cheapest to the
@@ -190,7 +226,7 @@ class HeatingOptimizer(Juham):
190
226
  Returns:
191
227
  list: sorted list of electricity prices
192
228
  """
193
- sh = sorted(hours, key=lambda x: x["Rank"])
229
+ sh = sorted(slot, key=lambda x: x["Rank"])
194
230
  ranked_hours: list[dict[str, Any]] = []
195
231
  for h in sh:
196
232
  utc_ts = h["Timestamp"]
@@ -223,16 +259,16 @@ class HeatingOptimizer(Juham):
223
259
  self.debug(
224
260
  f"{self.name} sorted {len(sh)} days of forecast starting at {timestampstr(ts_utc)}"
225
261
  )
226
- ranked_hours: list[dict[str, Any]] = []
262
+ ranked_slots: list[dict[str, Any]] = []
227
263
 
228
264
  for h in sh:
229
265
  utc_ts: float = float(h["ts"])
230
266
  if utc_ts >= ts_utc:
231
- ranked_hours.append(h)
267
+ ranked_slots.append(h)
232
268
  self.debug(
233
- f"{self.name} forecast sorted for the next {str(len(ranked_hours))} hours"
269
+ f"{self.name} forecast sorted for the next {str(len(ranked_slots))} hours"
234
270
  )
235
- return ranked_hours
271
+ return ranked_slots
236
272
 
237
273
  def on_spot(self, m: list[dict[str, Any]], ts_quantized: float) -> None:
238
274
  """Handle the spot prices.
@@ -398,19 +434,19 @@ class HeatingOptimizer(Juham):
398
434
  )
399
435
  return 1
400
436
 
401
- hour = timestamp_hour(ts)
437
+ slot : int = self.timestamp_slot(ts)
402
438
  state: int = -1
403
439
 
404
440
  # check if we are within the heating plan and see what the plan says
405
441
  for pp in self.heating_plan:
406
442
  ppts: float = pp["Timestamp"]
407
- h: float = timestamp_hour(ppts)
408
- if h == hour:
443
+ h: float = self.timestamp_slot(ppts)
444
+ if h == slot:
409
445
  state = pp["State"]
410
446
  break
411
447
 
412
448
  if state == -1:
413
- self.error(f"{self.name} cannot find heating plan for hour {hour}")
449
+ self.error(f"{self.name} cannot find heating plan for hour {slot}")
414
450
  return 0
415
451
 
416
452
  min_temp, max_temp = self.get_temperature_limits_for_current_month()
@@ -437,20 +473,20 @@ class HeatingOptimizer(Juham):
437
473
  def compute_uoi(
438
474
  self,
439
475
  price: float,
440
- hour: float,
476
+ slot: float,
441
477
  ) -> float:
442
478
  """Compute UOI - utilization optimization index.
443
479
 
444
480
  Args:
445
481
  price (float): effective price for this device
446
- hour (float) : the hour of the day
482
+ slot (float) : the slot of the day
447
483
 
448
484
  Returns:
449
485
  float: utilization optimization index
450
486
  """
451
487
 
452
- if not is_hour_within_schedule(
453
- hour, self.schedule_start_hour, self.schedule_stop_hour
488
+ if not self.is_slot_within_schedule(
489
+ slot, self.schedule_start_slot, self.schedule_stop_slot
454
490
  ):
455
491
  return 0.0
456
492
 
@@ -472,7 +508,6 @@ class HeatingOptimizer(Juham):
472
508
  requested_power (float): requested power
473
509
  available_solpower (float): current solar power forecast
474
510
  spot (float): spot price
475
- hour (float) : the hour of the day
476
511
 
477
512
  Returns:
478
513
  float: effective price for the requested power
@@ -489,6 +524,23 @@ class HeatingOptimizer(Juham):
489
524
 
490
525
  return effective_spot
491
526
 
527
+ def align_forecast_to_slots(self, solar_forecast: list[dict]) -> list[dict]:
528
+ """Resample hourly solar forecast to match slot interval."""
529
+ slots_per_hour = 3600 // self.energy_balancing_interval
530
+ expanded = []
531
+
532
+ for entry in solar_forecast: # each entry has "ts" (start of hour) and "solarenergy" (in kW)
533
+ start_ts = entry["Timestamp"]
534
+ for i in range(slots_per_hour):
535
+ slot_ts = start_ts + i * self.energy_balancing_interval
536
+ expanded.append({
537
+ "Timestamp": slot_ts,
538
+ "Solarenergy": entry["Solarenergy"] / slots_per_hour # split evenly
539
+ })
540
+
541
+ return expanded
542
+
543
+
492
544
  def create_power_plan(self) -> list[dict[Any, Any]]:
493
545
  """Create power plan.
494
546
 
@@ -520,10 +572,16 @@ class HeatingOptimizer(Juham):
520
572
  f"Have spot prices for the next {len(spots)} hours",
521
573
  "",
522
574
  )
523
- powers: list[dict[str, Any]] = []
524
- for s in self.ranked_solarpower:
525
- if s["ts"] >= ts_utc_quantized:
526
- powers.append({"Timestamp": s["ts"], "Solarenergy": s["solarenergy"]})
575
+
576
+ # Expand solar forecast to match spot price resolution
577
+ raw_powers = [
578
+ {"Timestamp": s["ts"], "Solarenergy": s["solarenergy"]}
579
+ for s in self.ranked_solarpower
580
+ if s["ts"] >= ts_utc_quantized
581
+ ]
582
+
583
+ powers : list[dict[str, Any]] = self.align_forecast_to_slots(raw_powers)
584
+
527
585
 
528
586
  num_powers: int = len(powers)
529
587
  if num_powers == 0:
@@ -537,8 +595,8 @@ class HeatingOptimizer(Juham):
537
595
  "",
538
596
  )
539
597
  hplan: list[dict[str, Any]] = []
540
- hour: float = 0
541
- if len(powers) >= 8: # at least 8 hours of solar energy forecast
598
+ slot: int = 0
599
+ if len(powers) >= 8: # at least 8 slot of solar energy forecast
542
600
  for spot, solar in zip(spots, powers):
543
601
  ts = spot["Timestamp"]
544
602
  solarenergy = solar["Solarenergy"] * 1000 # argh, this is in kW
@@ -546,8 +604,8 @@ class HeatingOptimizer(Juham):
546
604
  effective_price: float = self.compute_effective_price(
547
605
  self.radiator_power, solarenergy, spotprice
548
606
  )
549
- hour = timestamp_hour_local(ts, self.timezone)
550
- fom = self.compute_uoi(spotprice, hour)
607
+ slot = self.timestamp_slot(ts)
608
+ fom = self.compute_uoi(spotprice, slot)
551
609
  plan: dict[str, Any] = {
552
610
  "Timestamp": ts,
553
611
  "FOM": fom,
@@ -560,8 +618,8 @@ class HeatingOptimizer(Juham):
560
618
  solarenergy = 0.0
561
619
  spotprice = spot["PriceWithTax"]
562
620
  effective_price = spotprice # no free energy available
563
- hour = timestamp_hour_local(ts, self.timezone)
564
- fom = self.compute_uoi(effective_price, hour)
621
+ slot = timestamp_slot(ts)
622
+ fom = self.compute_uoi(effective_price, slot)
565
623
  plan = {
566
624
  "Timestamp": spot["Timestamp"],
567
625
  "FOM": fom,
@@ -571,15 +629,15 @@ class HeatingOptimizer(Juham):
571
629
 
572
630
  shplan = sorted(hplan, key=lambda x: x["FOM"], reverse=True)
573
631
 
574
- self.debug(f"{self.name} powerplan starts {starts} up to {len(shplan)} hours")
632
+ self.debug(f"{self.name} powerplan starts {starts} up to {len(shplan)} slots")
575
633
  return shplan
576
634
 
577
635
  def enable_relay(
578
- self, hour: float, spot: float, fom: float, end_hour: float
636
+ self, slot: int, spot: float, fom: float, end_slot: int
579
637
  ) -> bool:
580
638
  return (
581
- hour >= self.start_hour
582
- and hour < end_hour
639
+ slot >= self.start_slot
640
+ and slot < end_slot
583
641
  and float(spot) < self.spot_limit
584
642
  and fom > self.uoi_threshold
585
643
  )
@@ -593,18 +651,18 @@ class HeatingOptimizer(Juham):
593
651
 
594
652
  state = 0
595
653
  heating_plan: list[dict[str, Any]] = []
596
- hour: int = 0
654
+ slot: int = 0
597
655
  for hp in self.power_plan:
598
656
  ts: float = hp["Timestamp"]
599
657
  fom = hp["FOM"]
600
658
  spot = hp["Spot"]
601
- end_hour: float = self.start_hour + self.heating_hours_per_day
602
- local_hour: float = timestamp_hour_local(ts, self.timezone)
603
- schedule_on: bool = is_hour_within_schedule(
604
- local_hour, self.schedule_start_hour, self.schedule_stop_hour
659
+ end_slot: float = self.start_slot + self.heating_slots_per_day
660
+ slot: float = self.timestamp_slot(ts)
661
+ schedule_on: bool = self.is_slot_within_schedule(
662
+ slot, self.schedule_start_slot, self.schedule_stop_slot
605
663
  )
606
664
 
607
- if self.enable_relay(hour, spot, fom, end_hour) and schedule_on:
665
+ if self.enable_relay(slot, spot, fom, end_slot) and schedule_on:
608
666
  state = 1
609
667
  else:
610
668
  state = 0
@@ -619,7 +677,7 @@ class HeatingOptimizer(Juham):
619
677
 
620
678
  self.publish(self.topic_powerplan, json.dumps(heat), 1, False)
621
679
  heating_plan.append(heat)
622
- hour = hour + 1
680
+ slot = slot + 1
623
681
 
624
- self.info(f"{self.name} heating plan of {len(heating_plan)} hours created", "")
682
+ self.info(f"{self.name} heating plan of {len(heating_plan)} slots created", "")
625
683
  return heating_plan
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: juham-automation
3
- Version: 0.1.3
3
+ Version: 0.1.4
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>
@@ -56,9 +56,11 @@ Welcome to Juham™ - Juha's Ultimate Home Automation Masterpiece
56
56
  Project Description
57
57
  -------------------
58
58
 
59
- This package extends the ``juham_core`` package, providing home automation building blocks that address most common needs. It consists of two main sub-modules:
59
+ This package extends the ``juham_core`` package, providing home automation building blocks that address most common needs.
60
+ It consists of two main sub-modules:
60
61
 
61
62
  ``automation``:
63
+
62
64
  - **spothintafi**: Acquires electricity prices in Finland.
63
65
  - **watercirculator**: Automates a water circulator pump based on hot water temperature and motion detection.
64
66
  - **heatingoptimizer**: Controls hot water radiators based on temperature sensors and electricity price data.
@@ -66,6 +68,7 @@ This package extends the ``juham_core`` package, providing home automation build
66
68
  - **energybalancer**: Handles real-time energy balancing and net billing.
67
69
 
68
70
  ``ts``:
71
+
69
72
  - This folder contains time series recorders that listen for Juham™ topics and store the data in a time series database for later inspection.
70
73
 
71
74
  Project Status
@@ -73,7 +76,8 @@ Project Status
73
76
 
74
77
  **Current State**: **Alpha (Status 3)**
75
78
 
76
- All classes have been tested to some extent, and no known bugs have been reported. However, the code still requires work in terms of design and robustness. For example, electricity prices are currently hard-coded to use euros, but this should be configurable to support multiple currencies.
79
+ All classes have been tested to some extent, and no known bugs have been reported. However, the code still requires
80
+ work in terms of design and robustness.
77
81
 
78
82
 
79
83
  Project Links
@@ -3,8 +3,8 @@ 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
5
  juham_automation/automation/energybalancer.py,sha256=Mf9bK-Xo4zNalePL5EIGvlFObMkjWgeh76gvcU-ydIk,11532
6
- juham_automation/automation/energycostcalculator.py,sha256=OOvKhRbQ99wtnmoy9R3kGdAPMSkoDLa5GnZswpu52u0,11853
7
- juham_automation/automation/heatingoptimizer.py,sha256=M03r9sZLAPavjj0LlFXiVZqAKHN5ZUvcDVi2QR5yOrk,24684
6
+ juham_automation/automation/energycostcalculator.py,sha256=dYZxfnPtCw5FqIOAyBU0POf-ulTnZ8iUL45MEh4KLfE,13022
7
+ juham_automation/automation/heatingoptimizer.py,sha256=xFDalR86CGyyJ2z-MCDGUv0tOZW3_KSM-J39VIgJ404,26644
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.1.3.dist-info/licenses/LICENSE.rst,sha256=QVHD5V5_HSys2PdPdig_xKggDj8cGX33ALKqRsYyjtI,1089
21
- juham_automation-0.1.3.dist-info/METADATA,sha256=5Suhmsd8drHmj91gPngyVohtnA3sPdwsjsVIGYaQR7w,7122
22
- juham_automation-0.1.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
23
- juham_automation-0.1.3.dist-info/entry_points.txt,sha256=h-KzuKjmGPd4_iX_oiGvxx4IEc97dVbGGlhdh5ctbpI,605
24
- juham_automation-0.1.3.dist-info/top_level.txt,sha256=jfohvtocvX_gfT21AhJk7Iay5ZiQsS3HzrDjF7S4Qp0,17
25
- juham_automation-0.1.3.dist-info/RECORD,,
20
+ juham_automation-0.1.4.dist-info/licenses/LICENSE.rst,sha256=QVHD5V5_HSys2PdPdig_xKggDj8cGX33ALKqRsYyjtI,1089
21
+ juham_automation-0.1.4.dist-info/METADATA,sha256=ibE3-7LrwwYJygOyuLmrkWKFxfpTdoZPZtd3mlWoykQ,6992
22
+ juham_automation-0.1.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
23
+ juham_automation-0.1.4.dist-info/entry_points.txt,sha256=h-KzuKjmGPd4_iX_oiGvxx4IEc97dVbGGlhdh5ctbpI,605
24
+ juham_automation-0.1.4.dist-info/top_level.txt,sha256=jfohvtocvX_gfT21AhJk7Iay5ZiQsS3HzrDjF7S4Qp0,17
25
+ juham_automation-0.1.4.dist-info/RECORD,,