Signal._handlers was typed list[Callable[..., None]] and fire
accepted arbitrary **kwargs, so a typo at a fire site or a missing
parameter at a handler only blew up the moment a real service
event dispatched - hours into a run on a quiet network. mypy
could not catch dispatch mismatches when the contract shifted.
Lock the contract down: define a ServiceStateChangeHandler
Protocol describing the (zeroconf, service_type, name,
state_change) keyword signature, type Signal._handlers as a list
of that Protocol, and make Signal.fire keyword-only with the four
named parameters. register_handler / unregister_handler still
accept Callable[..., None] for back-compat and cast at the
boundary.