Compare commits
12
Commits
d8e604d64d
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9802982d73 | ||
|
|
ff39f4cb2f | ||
|
|
b76f107f01 | ||
|
|
f5a586c379 | ||
|
|
94754e6285 | ||
|
|
10c5b9e043 | ||
|
|
9f32a666c3 | ||
|
|
3130df69ce | ||
|
|
35856b2ba9 | ||
|
|
a5258b1985 | ||
|
|
42ae8f4bc4 | ||
|
|
2c87c6a6f3 |
No files matched your search
@@ -5,3 +5,4 @@ __pycache__/
|
||||
logs/
|
||||
apps/data/
|
||||
secrets.yaml
|
||||
.codex/
|
||||
+81
-8
@@ -32,7 +32,19 @@ class CalendarEvent:
|
||||
|
||||
class AlarmClock(SmartObject):
|
||||
|
||||
CALENDAR_RETRY_DELAYS = (2 * 60, 5 * 60)
|
||||
|
||||
@property
|
||||
def calendar_last_success(self):
|
||||
return self.dataset.get('calendar_last_success')
|
||||
|
||||
@calendar_last_success.setter
|
||||
def calendar_last_success(self, value):
|
||||
self.dataset['calendar_last_success'] = value
|
||||
|
||||
def on_initialize_smart_object(self):
|
||||
if self.dataset is None:
|
||||
self.dataset = {}
|
||||
self.switch_alarmclock_main = self.args['switch_alarmclock_main']
|
||||
self.switch_alarmclock_secondary = self.args['switch_alarmclock_secondary']
|
||||
self.inputdate_alarmclock_main = self.args['inputdate_alarmclock_main']
|
||||
@@ -46,6 +58,8 @@ class AlarmClock(SmartObject):
|
||||
self.handle_program_next_wakueup_cb = None
|
||||
self.handle_prewakueup_cb = None
|
||||
self.handle_wakueup_cb = None
|
||||
self.calendar_retry_handle = None
|
||||
self.calendar_retry_attempt = 0
|
||||
self.handle_switch_reveil_principal = self.listen_state(self.on_alarm_clock_set, self.switch_alarmclock_main)
|
||||
self.handle_switch_reveil_secondaire = self.listen_state(self.on_alarm_clock_set, self.switch_alarmclock_secondary)
|
||||
self.listen_state(self.on_alarm_clock_set, self.inputdate_alarmclock_main)
|
||||
@@ -64,17 +78,64 @@ class AlarmClock(SmartObject):
|
||||
|
||||
def refresh_cal(self):
|
||||
with self.log_group("refresh_cal"):
|
||||
new_cal = list()
|
||||
new_cal = list()
|
||||
#if self.load_cal_from_google_calendar(new_cal):
|
||||
if self.load_cal_from_ics(new_cal):
|
||||
if self.load_cal_from_ics(new_cal):
|
||||
self.calendar_last_success = datetime.datetime.now(datetime.timezone.utc)
|
||||
self.calendar_retry_attempt = 0
|
||||
self._cancel_calendar_retry()
|
||||
if not self.are_cal_identical(self.calendar,new_cal):
|
||||
self.calendar = new_cal
|
||||
self.log_info("Calendar has been updated!")
|
||||
self.print_cal(self.calendar)
|
||||
return True
|
||||
|
||||
else:
|
||||
self._schedule_calendar_retry()
|
||||
|
||||
return False
|
||||
|
||||
def _log_calendar_failure(self, reason):
|
||||
last_success = self.calendar_last_success
|
||||
if last_success is None:
|
||||
log = self.log_error
|
||||
freshness = 'no successful refresh recorded'
|
||||
else:
|
||||
age = datetime.datetime.now(datetime.timezone.utc) - last_success
|
||||
if age < datetime.timedelta(hours=24):
|
||||
log = self.log_info
|
||||
elif age < datetime.timedelta(hours=48):
|
||||
log = self.log_warning
|
||||
else:
|
||||
log = self.log_error
|
||||
freshness = (f'last_success={last_success.isoformat()}, '
|
||||
f'age={age.total_seconds() / 3600:.2f}h')
|
||||
log(f"Couldn't load calendar: {reason}; {freshness}")
|
||||
|
||||
def _schedule_calendar_retry(self):
|
||||
if self.calendar_retry_handle is not None:
|
||||
return
|
||||
if self.calendar_retry_attempt >= len(self.CALENDAR_RETRY_DELAYS):
|
||||
return
|
||||
delay = self.CALENDAR_RETRY_DELAYS[self.calendar_retry_attempt]
|
||||
self.calendar_retry_attempt += 1
|
||||
self.calendar_retry_handle = self.run_in(self.on_calendar_retry, delay)
|
||||
self.log_info(f"Calendar refresh retry scheduled in {delay // 60} minutes")
|
||||
|
||||
def _cancel_calendar_retry(self):
|
||||
if self.calendar_retry_handle is None:
|
||||
return
|
||||
if self.timer_running(self.calendar_retry_handle):
|
||||
self.cancel_timer(self.calendar_retry_handle)
|
||||
self.calendar_retry_handle = None
|
||||
|
||||
def on_calendar_retry(self, kwargs):
|
||||
self.calendar_retry_handle = None
|
||||
calendar_changed = self.refresh_cal()
|
||||
self.update_workday_status()
|
||||
if calendar_changed:
|
||||
self.update_wakeup_time()
|
||||
|
||||
def are_cal_identical(self,calendar1,calendar2):
|
||||
if len(calendar1) != len(calendar2): return False
|
||||
|
||||
@@ -93,7 +154,8 @@ class AlarmClock(SmartObject):
|
||||
|
||||
try:
|
||||
self.log_info(f"Retrieving calengar from outlook.office365.com")
|
||||
r = get("https://outlook.office365.com/owa/calendar/0c64359c70eb41cd9713fe92c470d3df@ubisoft.com/5edb3fb5150f4203b66d9cf8db3184be11626033572842691749/calendar.ics", timeout=10)#, headers=headers, verify=False)
|
||||
r = get("https://outlook.office365.com/owa/calendar/0c64359c70eb41cd9713fe92c470d3df@ubisoft.com/5edb3fb5150f4203b66d9cf8db3184be11626033572842691749/calendar.ics", timeout=(3, 5))#, headers=headers, verify=False)
|
||||
r.raise_for_status()
|
||||
#self.log_info(f"Processing Calendar")
|
||||
|
||||
today = datetime.datetime.combine(datetime.date.today(), datetime.time(0,0, 0))
|
||||
@@ -101,7 +163,7 @@ class AlarmClock(SmartObject):
|
||||
end_load_date = today + datetime.timedelta(days=2)
|
||||
#self.log_info("[" + str(start_load_date) + "]-[" + str(end_load_date) + "]")
|
||||
|
||||
calendar_ubi = icalendar.Calendar.from_ical(r.text)
|
||||
calendar_ubi = icalendar.Calendar.from_ical(r.content)
|
||||
for component in calendar_ubi.walk():
|
||||
if component.name == "VEVENT":
|
||||
event_summary = component.get('summary')
|
||||
@@ -137,11 +199,20 @@ class AlarmClock(SmartObject):
|
||||
calendar.append(CalendarEvent(start_datetime,end_datetime,event_type))
|
||||
else: self.log_warning(f"[Unknow event : {event_summary}]Start at {start_datetime},Finish at {end_datetime}")
|
||||
|
||||
except ValueError:
|
||||
self.log_error("Couldn't load calendar")
|
||||
except requests.exceptions.Timeout as error:
|
||||
self._log_calendar_failure(f"request timed out ({type(error).__name__})")
|
||||
return False
|
||||
except (requests.exceptions.ConnectionError,requests.exceptions.ReadTimeout):
|
||||
self.log_warning("Couldn't load calendar, request Timeout")
|
||||
except requests.exceptions.ConnectionError:
|
||||
self._log_calendar_failure("connection failed")
|
||||
return False
|
||||
except requests.exceptions.HTTPError as error:
|
||||
self._log_calendar_failure(f"HTTP {error.response.status_code}")
|
||||
return False
|
||||
except requests.exceptions.RequestException as error:
|
||||
self._log_calendar_failure(type(error).__name__)
|
||||
return False
|
||||
except (ValueError, TypeError, KeyError):
|
||||
self._log_calendar_failure("invalid ICS data")
|
||||
return False
|
||||
|
||||
#self.log_info(f"Calendar loaded")
|
||||
@@ -151,6 +222,8 @@ class AlarmClock(SmartObject):
|
||||
self.update_wakeup_time()
|
||||
|
||||
def on_refresh_cal(self, kwargs):
|
||||
self._cancel_calendar_retry()
|
||||
self.calendar_retry_attempt = 0
|
||||
calendar_changed = self.refresh_cal()
|
||||
self.update_workday_status()
|
||||
if calendar_changed:
|
||||
|
||||
@@ -115,7 +115,7 @@ bedroom_heatpump:
|
||||
auto: binary_sensor.heatpump_winter_mode or room_temperature > target_temperature + 1
|
||||
quiet: True
|
||||
|
||||
target_temperature: sensor.target_temperature_heatpump - 0.5
|
||||
target_temperature: "sensor.target_temperature_heatpump - (binary_sensor.heatpump_winter_mode ? 1 : 0.5)"
|
||||
hvac_mode: "binary_sensor.heatpump_winter_mode ? 'heat' : 'cool'"
|
||||
fan_mode: sensor.bedroom_heatpump_fan_mode
|
||||
|
||||
|
||||
@@ -27,3 +27,27 @@ notifications_manager:
|
||||
message:
|
||||
content: Good Bye
|
||||
timeout: 600
|
||||
|
||||
commut_rain_notification:
|
||||
recipients:
|
||||
pierre:
|
||||
services: notify/mobile_app_iphone_de_pierre
|
||||
conditions: binary_sensor.commut_rain
|
||||
|
||||
message:
|
||||
title: Pluie prévue sur votre trajet
|
||||
content: "Il va pleuvoir dans {sensor.commut_rain_minutes_until_rain} minutes"
|
||||
|
||||
power_off_notification:
|
||||
recipients:
|
||||
all:
|
||||
services:
|
||||
- notify/mobile_app_iphone_de_pierre
|
||||
- notify/mobile_app_mae
|
||||
conditions: binary_sensor.main_power_off and not binary_sensor.someone_home
|
||||
|
||||
message:
|
||||
title: Coupure de courant
|
||||
content: Le courant a été coupé
|
||||
auto_clear: true
|
||||
|
||||
@@ -60,10 +60,6 @@ occupancy_sensors:
|
||||
binary_sensor.guestroom_occupancy:
|
||||
trigger_conditions: binary_sensor.is_guestroom_sleeping or binary_sensor.day_interval_night
|
||||
blocking_conditions: not binary_sensor.someone_home or not input_boolean.guestroom_occupied_tonight
|
||||
retain_condition.desk_occupancy:
|
||||
retain_time: 5 * 60
|
||||
conditions:
|
||||
trigger_conditions:
|
||||
- sensor.desk_last_motion <= 5
|
||||
#- binary_sensor.alienware_r10
|
||||
#- sensor.macbook_m3_pro_de_pierre_frontmost_app != 'loginwindow' and sensor.macbook_m3_pro_de_pierre_displays == 2
|
||||
binary_sensor.desk_occupancy:
|
||||
trigger_conditions: sensor.desk_last_motion <= 5 or binary_sensor.is_desk_sleeping
|
||||
blocking_conditions: not binary_sensor.someone_home
|
||||
@@ -82,6 +82,8 @@ virtual_sensors:
|
||||
event_data:
|
||||
entity_id: sensor.shelly_button_red_signal_strength
|
||||
|
||||
binary_sensor.main_power_off: sensor.ups_code_d_etat != 'OL'
|
||||
|
||||
# living room sensors
|
||||
value_selector.living_room_ambiance_light:
|
||||
values:
|
||||
@@ -137,7 +139,7 @@ virtual_sensors:
|
||||
binary_sensor.p1p_is_active:
|
||||
trigger_conditions:
|
||||
- binary_sensor.p1p_is_printing
|
||||
- fan.garage_p1p_ventilateur_de_la_chambre or fan.garage_p1p_ventilateur_de_refroidissement
|
||||
- sensor.garage_p1p_vitesse_du_ventilateur_de_la_chambre or sensor.garage_p1p_vitesse_du_ventilateur_de_refroidissement
|
||||
- sensor.garage_p1p_temperature_cible_de_la_buse > 0 or sensor.garage_p1p_temperature_cible_du_lit > 0
|
||||
blocking_conditions: not switch.garage_p1p_plug
|
||||
|
||||
|
||||
@@ -4,11 +4,14 @@ from importlib import metadata
|
||||
import json
|
||||
import pickle
|
||||
import os
|
||||
import re
|
||||
from urllib.request import urlopen
|
||||
|
||||
class InformationsCollector(SmartObject):
|
||||
|
||||
APPDAEMON_PYPI_URL = "https://pypi.org/pypi/appdaemon/json"
|
||||
APPDAEMON_DOCKER_TAGS_URL = (
|
||||
"https://hub.docker.com/v2/repositories/acockburn/appdaemon/tags?page_size=100"
|
||||
)
|
||||
|
||||
def on_initialize_smart_object(self):
|
||||
super().on_initialize_smart_object()
|
||||
@@ -26,7 +29,7 @@ class InformationsCollector(SmartObject):
|
||||
sensor_name,
|
||||
state='unknown',
|
||||
attributes={
|
||||
'latest_official_version': self.dataset.get('appdaemon_latest_official_version'),
|
||||
'latest_official_version': self.dataset.get('appdaemon_latest_docker_version'),
|
||||
'update_available': None,
|
||||
},
|
||||
icon='mdi:package-up'
|
||||
@@ -143,9 +146,9 @@ class InformationsCollector(SmartObject):
|
||||
latest_official_version = self._get_latest_official_appdaemon_version()
|
||||
|
||||
if latest_official_version is not None:
|
||||
self.dataset['appdaemon_latest_official_version'] = latest_official_version
|
||||
self.dataset['appdaemon_latest_docker_version'] = latest_official_version
|
||||
else:
|
||||
latest_official_version = self.dataset.get('appdaemon_latest_official_version')
|
||||
latest_official_version = self.dataset.get('appdaemon_latest_docker_version')
|
||||
|
||||
update_available = self._is_appdaemon_update_available(installed_version, latest_official_version)
|
||||
|
||||
@@ -168,11 +171,22 @@ class InformationsCollector(SmartObject):
|
||||
|
||||
def _get_latest_official_appdaemon_version(self):
|
||||
try:
|
||||
with urlopen(self.APPDAEMON_PYPI_URL, timeout=10) as response:
|
||||
payload = json.load(response)
|
||||
return payload.get('info', {}).get('version')
|
||||
url = self.APPDAEMON_DOCKER_TAGS_URL
|
||||
versions = {}
|
||||
while url:
|
||||
with urlopen(url, timeout=10) as response:
|
||||
payload = json.load(response)
|
||||
for tag in payload['results']:
|
||||
name = tag.get('name', '')
|
||||
# Only stable version tags with published container images.
|
||||
if re.fullmatch(r'[0-9]+\.[0-9]+\.[0-9]+', name) and tag.get('images'):
|
||||
versions[tuple(map(int, name.split('.')))] = name
|
||||
url = payload.get('next')
|
||||
if not versions:
|
||||
raise ValueError("No stable AppDaemon Docker image found")
|
||||
return versions[max(versions)]
|
||||
except Exception as err:
|
||||
self.log_warning(f"Unable to fetch latest official AppDaemon version: {err}")
|
||||
self.log_warning(f"Unable to fetch latest official AppDaemon Docker version: {err}")
|
||||
return None
|
||||
|
||||
def _is_appdaemon_update_available(self, installed_version, latest_official_version):
|
||||
|
||||
@@ -3,7 +3,8 @@ import appdaemon.exceptions as ad_exception
|
||||
import ad_toolbox.smartcondition as SmartCondition
|
||||
import re
|
||||
import uuid
|
||||
from expressionparser import ParsingException
|
||||
from ad_toolbox.expressionparser import ParsingException
|
||||
from ad_toolbox.logger_interface import LoggerInterface
|
||||
|
||||
class Notification:
|
||||
ENTITY_PLACEHOLDER_PATTERN = re.compile(r"\{([^{}]+)\}")
|
||||
@@ -18,7 +19,7 @@ class Notification:
|
||||
for recipient in config["recipients"]:
|
||||
try: self.recipients[recipient] = SmartCondition.Evaluator(self.ad_api,self.config["recipients"][recipient]['conditions'],condition_name = name,on_succeed_cb = self.on_condition_succeed, on_fail_cb = self.on_conditon_fail,pass_condition_name_to_cb = recipient, trigger_callback_on_activation = False)
|
||||
except ParsingException as e:
|
||||
self.log_error(str(e))
|
||||
self.ad_api.log_error(str(e))
|
||||
continue
|
||||
|
||||
def on_conditon_fail(self, recipient):
|
||||
@@ -42,9 +43,9 @@ class Notification:
|
||||
data = { 'tag' : tag }
|
||||
|
||||
def clear_notification(service,tag):
|
||||
self.ad_api.log(f"Clear notification {data} from {service}")
|
||||
self.ad_api.log_info(f"Clear notification {data} from {service}")
|
||||
try: self.ad_api.call_service(service,message = message, data = data)
|
||||
except ad_exception.ServiceException: self.log_error(f"Invalid service: {service}")
|
||||
except ad_exception.ServiceException: self.ad_api.log_error(f"Invalid service: {service}")
|
||||
|
||||
recipient_yaml = self.config["recipients"][recipient]
|
||||
if isinstance(recipient_yaml['services'], str):
|
||||
@@ -65,11 +66,11 @@ class Notification:
|
||||
try:
|
||||
state = self.ad_api.get_state(entity_id)
|
||||
except Exception as e:
|
||||
self.log_error(f"Failed to resolve '{entity_id}' in notification content: {e}")
|
||||
self.ad_api.log_error(f"Failed to resolve '{entity_id}' in notification content: {e}")
|
||||
return match.group(0)
|
||||
|
||||
if state is None:
|
||||
self.log_error(f"Unable to resolve '{entity_id}' in notification content")
|
||||
self.ad_api.log_error(f"Unable to resolve '{entity_id}' in notification content")
|
||||
return match.group(0)
|
||||
|
||||
return str(state)
|
||||
@@ -96,9 +97,9 @@ class Notification:
|
||||
self.ad_api.run_in(self.on_timeout_expired,timeout,tag = data['tag'],recipient = recipient)
|
||||
|
||||
def send_notification(service):
|
||||
self.ad_api.log(f"Sending notification '{message}' to {service}{f' with data {data}' if data else ''}")
|
||||
self.ad_api.log_info(f"Sending notification '{message}' to {service}{f' with data {data}' if data else ''}")
|
||||
try: self.ad_api.call_service(service,title = title,message = message, data = data)
|
||||
except ad_exception.ServiceException: self.log_error(f"Invalid service: {service}")
|
||||
except ad_exception.ServiceException: self.ad_api.log_error(f"Invalid service: {service}")
|
||||
|
||||
recipient_yaml = self.config["recipients"][recipient]
|
||||
if isinstance(recipient_yaml['services'], str):
|
||||
@@ -107,17 +108,13 @@ class Notification:
|
||||
for service in recipient_yaml['services']:
|
||||
send_notification(service)
|
||||
|
||||
def log_error(self,message,stop_app = False):
|
||||
self.ad_api.log(message,level = "ERROR", log = "main_log")
|
||||
self.ad_api.log(message,level = "ERROR", log = "error_log")
|
||||
if stop_app:
|
||||
self.ad_api.stop_app(self.ad_api.name)
|
||||
|
||||
class NotificationsManager(hass.Hass):
|
||||
class NotificationsManager(hass.Hass, LoggerInterface):
|
||||
|
||||
def initialize(self):
|
||||
# Preserve the default app logger used by the previous direct log calls.
|
||||
self.initialize_logger_interface(self.get_ad_api(), default_log=None)
|
||||
self.notifications = dict()
|
||||
|
||||
if "notifications" in self.args:
|
||||
for notification in self.args["notifications"]:
|
||||
self.notifications[notification] = Notification(self,self.args["notifications"][notification],notification)
|
||||
self.notifications[notification] = Notification(self,self.args["notifications"][notification],notification)
|
||||
@@ -4,5 +4,5 @@ light_ambiance:
|
||||
entity: light.desk_ambiance
|
||||
|
||||
smart_conditions:
|
||||
trigger_conditions: sensor.desk_last_motion < 3 and binary_sensor.is_dark_outside
|
||||
trigger_conditions: binary_sensor.desk_occupancy and binary_sensor.is_dark_outside
|
||||
blocking_conditions: binary_sensor.is_desk_sleeping
|
||||
@@ -15,7 +15,7 @@ p1p_light:
|
||||
smart_conditions:
|
||||
unavaibility_result: Disabled
|
||||
trigger_conditions: binary_sensor.garage_occupancy or binary_sensor.garage_p1p_timelapse or camera.garage_p1p_camera == 'streaming'
|
||||
blocking_conditions: not binary_sensor.p1p_is_printing and not fan.garage_p1p_ventilateur_de_refroidissement and not fan.garage_p1p_ventilateur_de_la_chambre
|
||||
blocking_conditions: not binary_sensor.p1p_is_printing and not sensor.garage_p1p_vitesse_du_ventilateur_de_refroidissement and not sensor.garage_p1p_vitesse_du_ventilateur_de_la_chambre
|
||||
|
||||
p1p_switch:
|
||||
module: smartswitch
|
||||
|
||||
@@ -49,6 +49,13 @@ light_bookshelves:
|
||||
25: sensor.living_room_ambiance_light == 1
|
||||
100: True
|
||||
|
||||
light_christmas_tree:
|
||||
module: smartswitch
|
||||
class: SmartSwitch
|
||||
entity: switch.chrismas_tree
|
||||
|
||||
smart_conditions: sensor.living_room_ambiance_light >= 1
|
||||
|
||||
light_tree:
|
||||
module: smartlight
|
||||
class: SmartLight
|
||||
@@ -102,7 +109,7 @@ mezzanine_curtains:
|
||||
|
||||
close_conditions:
|
||||
trigger_conditions: binary_sensor.projector_on or not binary_sensor.projector_off_for_a_while
|
||||
disable_conditions: self == 'closed' and (binary_sensor.is_dark_outside or binary_sensor.is_anybody_sleeping)
|
||||
disable_conditions: binary_sensor.is_dark_outside or binary_sensor.is_anybody_sleeping
|
||||
|
||||
living_room_sleep_switch:
|
||||
module: smartswitch
|
||||
|
||||
+63
-6
@@ -7,6 +7,7 @@ import time
|
||||
import json
|
||||
import socket
|
||||
import threading
|
||||
from collections import deque
|
||||
import re
|
||||
from urllib.parse import urlparse
|
||||
|
||||
@@ -14,6 +15,7 @@ class Vertex2(SmartObject):
|
||||
def on_initialize_smart_object(self):
|
||||
# HASS can call initialize() again without calling terminate() first.
|
||||
self._stop_interrupt_listener()
|
||||
self._cancel_input_5v_refresh()
|
||||
self._interrupt_stop = threading.Event()
|
||||
self._status_requested = threading.Event()
|
||||
self._last_logged_status = None
|
||||
@@ -217,9 +219,28 @@ class Vertex2(SmartObject):
|
||||
if sensor.get_state() != 'unavailable':
|
||||
sensor.set_state(state='unavailable')
|
||||
|
||||
def _schedule_input_5v_refresh(self):
|
||||
# Device monitoring must run even when no HA input sensors are exposed.
|
||||
self.run_in(self._refresh_input_5v_sensors, 0.1)
|
||||
def _cancel_input_5v_refresh(self):
|
||||
handle = getattr(self, '_input_5v_refresh_handle', None)
|
||||
if handle is not None:
|
||||
self.cancel_timer(handle, silent=True)
|
||||
self._input_5v_refresh_handle = None
|
||||
self._input_5v_refresh_generation = getattr(self, '_input_5v_refresh_generation', 0) + 1
|
||||
self._input_5v_failure_since = None
|
||||
self._input_5v_failures = 0
|
||||
|
||||
def _schedule_input_5v_refresh(self, delay=1):
|
||||
# Coalesce bursts without postponing a pending retry or starving reads.
|
||||
if self._input_5v_refresh_handle is not None:
|
||||
return
|
||||
self._input_5v_refresh_generation += 1
|
||||
self._input_5v_refresh_handle = self.run_in(
|
||||
self._on_input_5v_refresh, delay, generation=self._input_5v_refresh_generation)
|
||||
|
||||
def _on_input_5v_refresh(self, kwargs):
|
||||
if kwargs['generation'] != self._input_5v_refresh_generation:
|
||||
return
|
||||
self._input_5v_refresh_handle = None
|
||||
self._refresh_input_5v_sensors()
|
||||
|
||||
def _refresh_input_5v_sensors(self, kwargs=None):
|
||||
if not self._vertex2_ready():
|
||||
@@ -233,11 +254,27 @@ class Vertex2(SmartObject):
|
||||
raise ValueError('expected an information object')
|
||||
except (RequestException, ValueError) as error:
|
||||
self._invalidate_input_5v_sensors()
|
||||
self.log_warning(f'Vertex2 input +5V readback failed: {error}')
|
||||
if not self._vertex2_ready():
|
||||
self._cancel_input_5v_refresh()
|
||||
return
|
||||
self._input_5v_failures += 1
|
||||
if self._input_5v_failure_since is None:
|
||||
self._input_5v_failure_since = time.monotonic()
|
||||
log = self.log_warning if self._input_5v_failures == 2 else self.log_info
|
||||
log(f'Vertex2 input +5V readback failed '
|
||||
f'(attempt={self._input_5v_failures}, {type(error).__name__}): {error}')
|
||||
if self._input_5v_failures == 1:
|
||||
self._schedule_input_5v_refresh(delay=5)
|
||||
return
|
||||
if not self._vertex2_ready():
|
||||
self._invalidate_input_5v_sensors()
|
||||
return
|
||||
if self._input_5v_failure_since is not None:
|
||||
self.log_info(
|
||||
f'Vertex2 input +5V readback restored after '
|
||||
f'{time.monotonic() - self._input_5v_failure_since:.1f}s '
|
||||
f'({self._input_5v_failures} failed attempts)')
|
||||
self._cancel_input_5v_refresh()
|
||||
states = {}
|
||||
for index in range(4):
|
||||
# Firmware uses 2 for present and 1 for absent, not boolean 1/0.
|
||||
@@ -442,11 +479,26 @@ class Vertex2(SmartObject):
|
||||
status_started = None
|
||||
reconfigure = True
|
||||
pending_interrupt_at = None
|
||||
recent_lines = deque(maxlen=12)
|
||||
received_bytes = 0
|
||||
interrupt_count = 0
|
||||
|
||||
def status_timeout_error():
|
||||
return OSError(
|
||||
'Vertex2 TCP status response timed out '
|
||||
f'(elapsed={time.monotonic() - status_started:.2f}s, limit=5s, '
|
||||
f'received={sorted(status)}, missing={sorted(expected - status.keys())}, '
|
||||
f'bytes={received_bytes}, interrupts={interrupt_count}, '
|
||||
f'reconfigure={reconfigure}, '
|
||||
f'recent_lines={list(recent_lines)!r}, partial_line={buffer[-256:]!r})')
|
||||
|
||||
while not interrupt_stop.is_set() and self._vertex2_powered():
|
||||
if status_started is None and self._status_requested.is_set():
|
||||
self._status_requested.clear()
|
||||
status = {}
|
||||
recent_lines.clear()
|
||||
received_bytes = 0
|
||||
interrupt_count = 0
|
||||
status_started = time.monotonic()
|
||||
request_interrupt_at = pending_interrupt_at
|
||||
pending_interrupt_at = None
|
||||
@@ -457,7 +509,7 @@ class Vertex2(SmartObject):
|
||||
data = connection.recv(1024)
|
||||
except socket.timeout:
|
||||
if status_started is not None and time.monotonic() - status_started > 5:
|
||||
raise OSError('Vertex2 TCP status response timed out')
|
||||
raise status_timeout_error()
|
||||
continue
|
||||
|
||||
if interrupt_stop.is_set() or not self._vertex2_powered():
|
||||
@@ -465,6 +517,7 @@ class Vertex2(SmartObject):
|
||||
if not data:
|
||||
raise OSError('Vertex2 closed the IP INTERRUPT connection')
|
||||
|
||||
received_bytes += len(data)
|
||||
buffer += data.decode('ascii', errors='ignore')
|
||||
if len(buffer) > 4096:
|
||||
raise OSError('Vertex2 TCP response exceeded buffer limit')
|
||||
@@ -472,7 +525,9 @@ class Vertex2(SmartObject):
|
||||
while '\n' in buffer:
|
||||
line, buffer = buffer.split('\n', 1)
|
||||
line = line.strip()
|
||||
recent_lines.append(line[:256])
|
||||
if line == 'int':
|
||||
interrupt_count += 1
|
||||
pending_interrupt_at = time.monotonic()
|
||||
self._status_requested.set()
|
||||
elif status_started is not None:
|
||||
@@ -497,7 +552,7 @@ class Vertex2(SmartObject):
|
||||
status_started = None
|
||||
|
||||
if status_started is not None and time.monotonic() - status_started > 5:
|
||||
raise OSError('Vertex2 TCP status response timed out')
|
||||
raise status_timeout_error()
|
||||
|
||||
except (OSError, ValueError) as error:
|
||||
if interrupt_stop.is_set():
|
||||
@@ -557,6 +612,7 @@ class Vertex2(SmartObject):
|
||||
self._command_processing_handle = None
|
||||
|
||||
def terminate(self):
|
||||
self._cancel_input_5v_refresh()
|
||||
if getattr(self, '_input_select_config', None):
|
||||
self._publish_input_select('/availability', 'offline')
|
||||
self._stop_interrupt_listener()
|
||||
@@ -721,6 +777,7 @@ class Vertex2(SmartObject):
|
||||
)
|
||||
|
||||
def _suspend_vertex2(self):
|
||||
self._cancel_input_5v_refresh()
|
||||
self._invalidate_input_5v_sensors()
|
||||
self._reset_no_signal_spam()
|
||||
self._invalidate_plus5_sensors()
|
||||
|
||||
Reference in new issue
Block a user