boneIO 1.4.0.dev13__py3-none-any.whl → 1.4.0.dev14__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.
@@ -691,6 +691,13 @@ class IrrigationController:
691
691
  self._active_zone_remaining_s = duration
692
692
  self._state = ControllerState.RUNNING
693
693
 
694
+ # Publish state immediately so the UI (frontend + HA) shows
695
+ # RUNNING right away, before any pump/valve delay sleeps.
696
+ # The timer has not been armed yet so zone_end_time will be
697
+ # empty, but a second publish happens after hardware activation.
698
+ self._run_start_utc = utcnow()
699
+ await self.publish_all_states()
700
+
694
701
  if src is not None:
695
702
  if src.pump_start_valve_delay_s > 0:
696
703
  # Source first (pump+valve), then zone valve after delay
@@ -130,8 +130,16 @@ class InputManager:
130
130
  return False
131
131
  return False
132
132
 
133
- def configure_single_input(configure_sensor_func: Callable, gpio: dict) -> None:
134
- """Configure a single input (event or binary sensor)."""
133
+ def configure_single_input(
134
+ configure_sensor_func: Callable, gpio: dict, expected_class: type
135
+ ) -> None:
136
+ """Configure a single input (event or binary sensor).
137
+
138
+ Args:
139
+ configure_sensor_func: The _configure_event_sensor or _configure_binary_sensor method.
140
+ gpio: GPIO configuration dictionary.
141
+ expected_class: The expected class type (GpioEventButton or GpioInputBinarySensor).
142
+ """
135
143
  # Work on a copy to avoid modifying the cached config
136
144
  gpio_copy = gpio.copy()
137
145
 
@@ -149,6 +157,18 @@ class InputManager:
149
157
 
150
158
  existing_input = self._inputs.get(input_id, None) if reload_config else None
151
159
 
160
+ # Guard: if the existing input is a different class (type changed,
161
+ # e.g. event → binary_sensor), do NOT pass it as existing_input.
162
+ # A new object with the correct detector must be created instead.
163
+ if existing_input is not None and not isinstance(existing_input, expected_class):
164
+ _LOGGER.info(
165
+ "Input %s type changed from %s to %s, creating new input object",
166
+ input_id,
167
+ type(existing_input).__name__,
168
+ expected_class.__name__,
169
+ )
170
+ existing_input = None
171
+
152
172
  input_device = configure_sensor_func(
153
173
  gpio=gpio_copy,
154
174
  pin=pin,
@@ -172,14 +192,22 @@ class InputManager:
172
192
  # Configure event buttons
173
193
  for gpio in self._event_pins:
174
194
  try:
175
- configure_single_input(configure_sensor_func=self._configure_event_sensor, gpio=gpio)
195
+ configure_single_input(
196
+ configure_sensor_func=self._configure_event_sensor,
197
+ gpio=gpio,
198
+ expected_class=GpioEventButton,
199
+ )
176
200
  except GPIOInputException as err:
177
201
  _LOGGER.error("Failed to configure event input: %s", err)
178
202
 
179
203
  # Configure binary sensors
180
204
  for gpio in self._binary_pins:
181
205
  try:
182
- configure_single_input(configure_sensor_func=self._configure_binary_sensor, gpio=gpio)
206
+ configure_single_input(
207
+ configure_sensor_func=self._configure_binary_sensor,
208
+ gpio=gpio,
209
+ expected_class=GpioInputBinarySensor,
210
+ )
183
211
  except GPIOInputException as err:
184
212
  _LOGGER.error("Failed to configure binary sensor: %s", err)
185
213
 
@@ -490,21 +518,25 @@ class InputManager:
490
518
  # Get config from ConfigHelper (already reloaded by Manager)
491
519
  config = self._manager._config_helper.get_config()
492
520
 
493
- # Build map of new inputs from config (input_id -> {pin, area})
521
+ # Build map of new inputs from config (input_id -> {pin, area, ha_type})
494
522
  # Note: boneio_input is normalized to lowercase to match yaml_util.py behavior
495
523
  new_input_map: dict[str, dict] = {}
496
- for gpio in config.get(EVENT_ENTITY, []) + config.get(BINARY_SENSOR, []):
497
- pin = gpio.get("pin")
498
- # Determine input ID (same logic as in _configure_event_sensor/_configure_binary_sensor)
499
- if "id" in gpio:
500
- input_id = gpio["id"]
501
- elif "boneio_input" in gpio:
502
- input_id = gpio["boneio_input"].lower()
503
- elif pin:
504
- input_id = pin
505
- else:
506
- continue
507
- new_input_map[input_id] = {"pin": pin, "area": gpio.get("area")}
524
+ for ha_type, gpio_list in [
525
+ (EVENT_ENTITY, config.get(EVENT_ENTITY, [])),
526
+ (BINARY_SENSOR, config.get(BINARY_SENSOR, [])),
527
+ ]:
528
+ for gpio in gpio_list:
529
+ pin = gpio.get("pin")
530
+ # Determine input ID (same logic as in _configure_event_sensor/_configure_binary_sensor)
531
+ if "id" in gpio:
532
+ input_id = gpio["id"]
533
+ elif "boneio_input" in gpio:
534
+ input_id = gpio["boneio_input"].lower()
535
+ elif pin:
536
+ input_id = pin
537
+ else:
538
+ continue
539
+ new_input_map[input_id] = {"pin": pin, "area": gpio.get("area"), "ha_type": ha_type}
508
540
 
509
541
  # Find inputs to remove (in current config but not in new config)
510
542
  current_input_ids = set(self._inputs.keys())
@@ -512,12 +544,29 @@ class InputManager:
512
544
 
513
545
  inputs_to_remove = current_input_ids - new_input_ids
514
546
 
515
- _LOGGER.info(f"Input reload: current={current_input_ids}, new={new_input_ids}, to_remove={inputs_to_remove}")
547
+ _LOGGER.info("Input reload: current=%s, new=%s, to_remove=%s", current_input_ids, new_input_ids, inputs_to_remove)
548
+
549
+ # Detect inputs whose type changed (event <-> binary_sensor).
550
+ # These must be removed and re-created with the correct detector class.
551
+ inputs_type_changed: set[str] = set()
552
+ for input_id, new_info in new_input_map.items():
553
+ existing = self._inputs.get(input_id)
554
+ if existing is None:
555
+ continue
556
+ new_ha_type = new_info["ha_type"]
557
+ # GpioEventButton has input_type=INPUT, GpioInputBinarySensor has input_type=INPUT_SENSOR
558
+ if new_ha_type == EVENT_ENTITY and not isinstance(existing, GpioEventButton):
559
+ inputs_type_changed.add(input_id)
560
+ elif new_ha_type == BINARY_SENSOR and not isinstance(existing, GpioInputBinarySensor):
561
+ inputs_type_changed.add(input_id)
562
+
563
+ if inputs_type_changed:
564
+ _LOGGER.info("Input type changed for: %s — will recreate with new detector", inputs_type_changed)
516
565
 
517
566
  # Remove deleted inputs from internal state (GPIO pin stays registered - minimal overhead)
518
567
  ha_discovery_changed = False
519
568
 
520
- for input_id in inputs_to_remove:
569
+ for input_id in inputs_to_remove | inputs_type_changed:
521
570
  input_device = self._inputs.get(input_id)
522
571
  if input_device:
523
572
  old_area = getattr(input_device, "area", None)
@@ -528,7 +577,7 @@ class InputManager:
528
577
 
529
578
  # Remove from internal state (GPIO detector will be replaced by new input class)
530
579
  del self._inputs[input_id]
531
- _LOGGER.info(f"Removed input {input_id}")
580
+ _LOGGER.info("Removed input %s (type_changed=%s)", input_id, input_id in inputs_type_changed)
532
581
 
533
582
  # Check for area changes on remaining inputs
534
583
  for input_id, input_device in self._inputs.items():
@@ -537,7 +586,7 @@ class InputManager:
537
586
 
538
587
  if old_area != new_area:
539
588
  ha_discovery_changed = True
540
- _LOGGER.info(f"Input {input_id} area changed: {old_area} -> {new_area}, removing old HA Discovery")
589
+ _LOGGER.info("Input %s area changed: %s -> %s, removing old HA Discovery", input_id, old_area, new_area)
541
590
  self._remove_input_ha_discovery(input_id, old_area)
542
591
 
543
592
  # Wait for HA to process the removal before sending new discovery
@@ -547,6 +596,7 @@ class InputManager:
547
596
 
548
597
  # _configure_inputs with reload_config=True will:
549
598
  # - Update existing inputs (actions, area, name)
599
+ # - Create new inputs for type-changed entries (old objects removed above)
550
600
  # - Add new inputs if their GPIO pin is already registered
551
601
  self._configure_inputs(reload_config=True)
552
602
 
@@ -541,7 +541,7 @@ class Oled:
541
541
  else:
542
542
  self._next_screen()
543
543
 
544
- if not self._cancel_sleep_handle and self._sleep_timeout.total_seconds > 0:
544
+ if self._sleep_timeout.total_seconds > 0:
545
545
  self.start_sleep_timer()
546
546
 
547
547
  def _update_display(self) -> None:
boneio/version.py CHANGED
@@ -1,2 +1,2 @@
1
1
  # flake8: noqa
2
- __version__ = "1.4.0dev13"
2
+ __version__ = "1.4.0dev14"