Protect status usage vs grpc protocol changes
Change all uses of status data protobuf messages so that they will not crash the calling script in the case where the grpc protocol removes the message, field, or enum value being accessed. Instead, they will return None for the affected field (which most calling scripts interpret as "no data") or raise the same error as when the dish is not reachable. This makes the code a little less readable, but it's better than breaking every time the protocol obsoletes fields. mypy now complains about the return types for some fields that are now technically optional but are not marked as such in the type data, because the type data reflects what will currently be returned, not what may turn to None in the future if the protocol changes. This is for issue #66.
This commit is contained in:
parent
0298ce2106
commit
8b1d81b2bb
1 changed files with 68 additions and 40 deletions
108
starlink_grpc.py
108
starlink_grpc.py
|
@ -535,6 +535,8 @@ class GrpcError(Exception):
|
||||||
msg = e.details()
|
msg = e.details()
|
||||||
elif isinstance(e, grpc.RpcError):
|
elif isinstance(e, grpc.RpcError):
|
||||||
msg = "Unknown communication or service error"
|
msg = "Unknown communication or service error"
|
||||||
|
elif isinstance(e, (AttributeError, ValueError)):
|
||||||
|
msg = "Protocol error"
|
||||||
else:
|
else:
|
||||||
msg = str(e)
|
msg = str(e)
|
||||||
super().__init__(msg, *args, **kwargs)
|
super().__init__(msg, *args, **kwargs)
|
||||||
|
@ -617,8 +619,11 @@ def status_field_names(context: Optional[ChannelContext] = None):
|
||||||
except grpc.RpcError as e:
|
except grpc.RpcError as e:
|
||||||
raise GrpcError(e) from e
|
raise GrpcError(e) from e
|
||||||
alert_names = []
|
alert_names = []
|
||||||
for field in dish_pb2.DishAlerts.DESCRIPTOR.fields:
|
try:
|
||||||
alert_names.append("alert_" + field.name)
|
for field in dish_pb2.DishAlerts.DESCRIPTOR.fields:
|
||||||
|
alert_names.append("alert_" + field.name)
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
|
||||||
return _field_names(StatusDict), _field_names(ObstructionDict), alert_names
|
return _field_names(StatusDict), _field_names(ObstructionDict), alert_names
|
||||||
|
|
||||||
|
@ -646,8 +651,12 @@ def status_field_types(context: Optional[ChannelContext] = None):
|
||||||
call_with_channel(resolve_imports, context=context)
|
call_with_channel(resolve_imports, context=context)
|
||||||
except grpc.RpcError as e:
|
except grpc.RpcError as e:
|
||||||
raise GrpcError(e) from e
|
raise GrpcError(e) from e
|
||||||
return (_field_types(StatusDict), _field_types(ObstructionDict),
|
num_alerts = 0
|
||||||
[bool] * len(dish_pb2.DishAlerts.DESCRIPTOR.fields))
|
try:
|
||||||
|
num_alerts = len(dish_pb2.DishAlerts.DESCRIPTOR.fields)
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
return (_field_types(StatusDict), _field_types(ObstructionDict), [bool] * num_alerts)
|
||||||
|
|
||||||
|
|
||||||
def get_status(context: Optional[ChannelContext] = None):
|
def get_status(context: Optional[ChannelContext] = None):
|
||||||
|
@ -661,6 +670,9 @@ def get_status(context: Optional[ChannelContext] = None):
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
grpc.RpcError: Communication or service error.
|
grpc.RpcError: Communication or service error.
|
||||||
|
AttributeError, ValueError: Protocol error. Either the target is not a
|
||||||
|
Starlink user terminal or the grpc protocol has changed in a way
|
||||||
|
this module cannot handle.
|
||||||
"""
|
"""
|
||||||
def grpc_call(channel):
|
def grpc_call(channel):
|
||||||
if imports_pending:
|
if imports_pending:
|
||||||
|
@ -689,7 +701,7 @@ def get_id(context: Optional[ChannelContext] = None) -> str:
|
||||||
try:
|
try:
|
||||||
status = get_status(context)
|
status = get_status(context)
|
||||||
return status.device_info.id
|
return status.device_info.id
|
||||||
except grpc.RpcError as e:
|
except (AttributeError, ValueError, grpc.RpcError) as e:
|
||||||
raise GrpcError(e) from e
|
raise GrpcError(e) from e
|
||||||
|
|
||||||
|
|
||||||
|
@ -711,62 +723,78 @@ def status_data(
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
status = get_status(context)
|
status = get_status(context)
|
||||||
except grpc.RpcError as e:
|
except (AttributeError, ValueError, grpc.RpcError) as e:
|
||||||
raise GrpcError(e) from e
|
raise GrpcError(e) from e
|
||||||
|
|
||||||
if status.HasField("outage"):
|
try:
|
||||||
if status.outage.cause == dish_pb2.DishOutage.Cause.NO_SCHEDULE:
|
if status.HasField("outage"):
|
||||||
# Special case translate this to equivalent old name
|
if status.outage.cause == dish_pb2.DishOutage.Cause.NO_SCHEDULE:
|
||||||
state = "SEARCHING"
|
# Special case translate this to equivalent old name
|
||||||
|
state = "SEARCHING"
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
state = dish_pb2.DishOutage.Cause.Name(status.outage.cause)
|
||||||
|
except ValueError:
|
||||||
|
# Unlikely, but possible if dish is running newer firmware
|
||||||
|
# than protocol data pulled via reflection
|
||||||
|
state = str(status.outage.cause)
|
||||||
else:
|
else:
|
||||||
state = dish_pb2.DishOutage.Cause.Name(status.outage.cause)
|
state = "CONNECTED"
|
||||||
else:
|
except (AttributeError, ValueError):
|
||||||
state = "CONNECTED"
|
state = "UNKNOWN"
|
||||||
|
|
||||||
# More alerts may be added in future, so in addition to listing them
|
# More alerts may be added in future, so in addition to listing them
|
||||||
# individually, provide a bit field based on field numbers of the
|
# individually, provide a bit field based on field numbers of the
|
||||||
# DishAlerts message.
|
# DishAlerts message.
|
||||||
alerts = {}
|
alerts = {}
|
||||||
alert_bits = 0
|
alert_bits = 0
|
||||||
for field in status.alerts.DESCRIPTOR.fields:
|
try:
|
||||||
value = getattr(status.alerts, field.name)
|
for field in status.alerts.DESCRIPTOR.fields:
|
||||||
alerts["alert_" + field.name] = value
|
value = getattr(status.alerts, field.name, False)
|
||||||
if field.number < 65:
|
alerts["alert_" + field.name] = value
|
||||||
alert_bits |= (1 if value else 0) << (field.number - 1)
|
if field.number < 65:
|
||||||
|
alert_bits |= (1 if value else 0) << (field.number - 1)
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
|
||||||
if (status.obstruction_stats.avg_prolonged_obstruction_duration_s > 0.0
|
obstruction_duration = None
|
||||||
and not math.isnan(status.obstruction_stats.avg_prolonged_obstruction_interval_s)):
|
obstruction_interval = None
|
||||||
obstruction_duration = status.obstruction_stats.avg_prolonged_obstruction_duration_s
|
obstruction_stats = getattr(status, "obstruction_stats", None)
|
||||||
obstruction_interval = status.obstruction_stats.avg_prolonged_obstruction_interval_s
|
if obstruction_stats is not None:
|
||||||
else:
|
try:
|
||||||
obstruction_duration = None
|
if (obstruction_stats.avg_prolonged_obstruction_duration_s > 0.0
|
||||||
obstruction_interval = None
|
and not math.isnan(obstruction_stats.avg_prolonged_obstruction_interval_s)):
|
||||||
|
obstruction_duration = obstruction_stats.avg_prolonged_obstruction_duration_s
|
||||||
|
obstruction_interval = obstruction_stats.avg_prolonged_obstruction_interval_s
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
device_info = getattr(status, "device_info", None)
|
||||||
return {
|
return {
|
||||||
"id": status.device_info.id,
|
"id": getattr(device_info, "id", None),
|
||||||
"hardware_version": status.device_info.hardware_version,
|
"hardware_version": getattr(device_info, "hardware_version", None),
|
||||||
"software_version": status.device_info.software_version,
|
"software_version": getattr(device_info, "software_version", None),
|
||||||
"state": state,
|
"state": state,
|
||||||
"uptime": status.device_state.uptime_s,
|
"uptime": getattr(getattr(status, "device_state", None), "uptime_s", None),
|
||||||
"snr": None, # obsoleted in grpc service
|
"snr": None, # obsoleted in grpc service
|
||||||
"seconds_to_first_nonempty_slot": status.seconds_to_first_nonempty_slot,
|
"seconds_to_first_nonempty_slot": getattr(status, "seconds_to_first_nonempty_slot", None),
|
||||||
"pop_ping_drop_rate": status.pop_ping_drop_rate,
|
"pop_ping_drop_rate": getattr(status, "pop_ping_drop_rate", None),
|
||||||
"downlink_throughput_bps": status.downlink_throughput_bps,
|
"downlink_throughput_bps": getattr(status, "downlink_throughput_bps", None),
|
||||||
"uplink_throughput_bps": status.uplink_throughput_bps,
|
"uplink_throughput_bps": getattr(status, "uplink_throughput_bps", None),
|
||||||
"pop_ping_latency_ms": status.pop_ping_latency_ms,
|
"pop_ping_latency_ms": getattr(status, "pop_ping_latency_ms", None),
|
||||||
"alerts": alert_bits,
|
"alerts": alert_bits,
|
||||||
"fraction_obstructed": status.obstruction_stats.fraction_obstructed,
|
"fraction_obstructed": getattr(obstruction_stats, "fraction_obstructed", None),
|
||||||
"currently_obstructed": status.obstruction_stats.currently_obstructed,
|
"currently_obstructed": getattr(obstruction_stats, "currently_obstructed", None),
|
||||||
"seconds_obstructed": None, # obsoleted in grpc service
|
"seconds_obstructed": None, # obsoleted in grpc service
|
||||||
"obstruction_duration": obstruction_duration,
|
"obstruction_duration": obstruction_duration,
|
||||||
"obstruction_interval": obstruction_interval,
|
"obstruction_interval": obstruction_interval,
|
||||||
"direction_azimuth": status.boresight_azimuth_deg,
|
"direction_azimuth": getattr(status, "boresight_azimuth_deg", None),
|
||||||
"direction_elevation": status.boresight_elevation_deg,
|
"direction_elevation": getattr(status, "boresight_elevation_deg", None),
|
||||||
"is_snr_above_noise_floor": status.is_snr_above_noise_floor,
|
"is_snr_above_noise_floor": getattr(status, "is_snr_above_noise_floor", None),
|
||||||
}, {
|
}, {
|
||||||
"wedges_fraction_obstructed[]": [None] * 12, # obsoleted in grpc service
|
"wedges_fraction_obstructed[]": [None] * 12, # obsoleted in grpc service
|
||||||
"raw_wedges_fraction_obstructed[]": [None] * 12, # obsoleted in grpc service
|
"raw_wedges_fraction_obstructed[]": [None] * 12, # obsoleted in grpc service
|
||||||
"valid_s": status.obstruction_stats.valid_s,
|
"valid_s": getattr(obstruction_stats, "valid_s", None),
|
||||||
}, alerts
|
}, alerts
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue