1 """Sensor for the CityBikes data."""
3 from __future__
import annotations
6 from datetime
import timedelta
10 import voluptuous
as vol
14 PLATFORM_SCHEMA
as SENSOR_PLATFORM_SCHEMA,
41 _LOGGER = logging.getLogger(__name__)
43 ATTR_EMPTY_SLOTS =
"empty_slots"
45 ATTR_FREE_BIKES =
"free_bikes"
46 ATTR_NETWORK =
"network"
47 ATTR_NETWORKS_LIST =
"networks"
48 ATTR_STATIONS_LIST =
"stations"
49 ATTR_TIMESTAMP =
"timestamp"
52 CONF_NETWORK =
"network"
53 CONF_STATIONS_LIST =
"stations"
55 DEFAULT_ENDPOINT =
"https://api.citybik.es/{uri}"
56 PLATFORM =
"citybikes"
58 MONITORED_NETWORKS =
"monitored-networks"
60 NETWORKS_URI =
"v2/networks"
66 STATIONS_URI =
"v2/networks/{uid}?fields=network.stations"
68 CITYBIKES_ATTRIBUTION = (
69 "Information provided by the CityBikes Project (https://citybik.es/#about)"
72 CITYBIKES_NETWORKS =
"citybikes_networks"
74 PLATFORM_SCHEMA = vol.All(
75 cv.has_at_least_one_key(CONF_RADIUS, CONF_STATIONS_LIST),
76 SENSOR_PLATFORM_SCHEMA.extend(
78 vol.Optional(CONF_NAME, default=
""): cv.string,
79 vol.Optional(CONF_NETWORK): cv.string,
80 vol.Inclusive(CONF_LATITUDE,
"coordinates"): cv.latitude,
81 vol.Inclusive(CONF_LONGITUDE,
"coordinates"): cv.longitude,
82 vol.Optional(CONF_RADIUS,
"station_filter"): cv.positive_int,
83 vol.Optional(CONF_STATIONS_LIST,
"station_filter"): vol.All(
84 cv.ensure_list, vol.Length(min=1), [cv.string]
90 NETWORK_SCHEMA = vol.Schema(
92 vol.Required(ATTR_ID): cv.string,
93 vol.Required(ATTR_NAME): cv.string,
94 vol.Required(ATTR_LOCATION): vol.Schema(
96 vol.Required(ATTR_LATITUDE): cv.latitude,
97 vol.Required(ATTR_LONGITUDE): cv.longitude,
99 extra=vol.REMOVE_EXTRA,
102 extra=vol.REMOVE_EXTRA,
105 NETWORKS_RESPONSE_SCHEMA = vol.Schema(
106 {vol.Required(ATTR_NETWORKS_LIST): [NETWORK_SCHEMA]}
109 STATION_SCHEMA = vol.Schema(
111 vol.Required(ATTR_FREE_BIKES): cv.positive_int,
112 vol.Required(ATTR_EMPTY_SLOTS): vol.Any(cv.positive_int,
None),
113 vol.Required(ATTR_LATITUDE): cv.latitude,
114 vol.Required(ATTR_LONGITUDE): cv.longitude,
115 vol.Required(ATTR_ID): cv.string,
116 vol.Required(ATTR_NAME): cv.string,
117 vol.Required(ATTR_TIMESTAMP): cv.string,
118 vol.Optional(ATTR_EXTRA): vol.Schema(
119 {vol.Optional(ATTR_UID): cv.string}, extra=vol.REMOVE_EXTRA
122 extra=vol.REMOVE_EXTRA,
125 STATIONS_RESPONSE_SCHEMA = vol.Schema(
127 vol.Required(ATTR_NETWORK): vol.Schema(
128 {vol.Required(ATTR_STATIONS_LIST): [STATION_SCHEMA]}, extra=vol.REMOVE_EXTRA
135 """Error to indicate a CityBikes API request has failed."""
139 """Perform a request to CityBikes API endpoint, and parse the response."""
143 async
with asyncio.timeout(REQUEST_TIMEOUT):
144 req = await session.get(DEFAULT_ENDPOINT.format(uri=uri))
146 json_response = await req.json()
147 return schema(json_response)
148 except (TimeoutError, aiohttp.ClientError):
149 _LOGGER.error(
"Could not connect to CityBikes API endpoint")
151 _LOGGER.error(
"Received non-JSON data from CityBikes API endpoint")
152 except vol.Invalid
as err:
153 _LOGGER.error(
"Received unexpected JSON from CityBikes API endpoint: %s", err)
154 raise CityBikesRequestError
160 async_add_entities: AddEntitiesCallback,
161 discovery_info: DiscoveryInfoType |
None =
None,
163 """Set up the CityBikes platform."""
164 if PLATFORM
not in hass.data:
165 hass.data[PLATFORM] = {MONITORED_NETWORKS: {}}
167 latitude = config.get(CONF_LATITUDE, hass.config.latitude)
168 longitude = config.get(CONF_LONGITUDE, hass.config.longitude)
169 network_id = config.get(CONF_NETWORK)
170 stations_list = set(config.get(CONF_STATIONS_LIST, []))
171 radius = config.get(CONF_RADIUS, 0)
172 name = config[CONF_NAME]
173 if hass.config.units
is US_CUSTOMARY_SYSTEM:
174 radius = DistanceConverter.convert(
175 radius, UnitOfLength.FEET, UnitOfLength.METERS
182 network_id = await networks.get_closest_network_id(latitude, longitude)
184 if network_id
not in hass.data[PLATFORM][MONITORED_NETWORKS]:
186 hass.data[PLATFORM][MONITORED_NETWORKS][network_id] = network
187 hass.async_create_task(network.async_refresh())
190 network = hass.data[PLATFORM][MONITORED_NETWORKS][network_id]
192 await network.ready.wait()
195 for station
in network.stations:
196 dist = location.distance(
197 latitude, longitude, station[ATTR_LATITUDE], station[ATTR_LONGITUDE]
199 station_id = station[ATTR_ID]
200 station_uid =
str(station.get(ATTR_EXTRA, {}).
get(ATTR_UID,
""))
202 if radius > dist
or stations_list.intersection((station_id, station_uid)):
204 uid = f
"{network.network_id}_{name}_{station_id}"
206 uid = f
"{network.network_id}_{station_id}"
214 """Represent all CityBikes networks."""
217 """Initialize the networks instance."""
223 """Return the id of the network closest to provided location."""
228 self.
hasshass, NETWORKS_URI, NETWORKS_RESPONSE_SCHEMA
230 self.
networksnetworks = networks[ATTR_NETWORKS_LIST]
231 except CityBikesRequestError
as err:
232 raise PlatformNotReady
from err
236 for network
in self.
networksnetworks:
237 network_latitude = network[ATTR_LOCATION][ATTR_LATITUDE]
238 network_longitude = network[ATTR_LOCATION][ATTR_LONGITUDE]
239 dist = location.distance(
240 latitude, longitude, network_latitude, network_longitude
242 if minimum_dist
is None or dist < minimum_dist:
244 result = network[ATTR_ID]
252 """Thin wrapper around a CityBikes network object."""
255 """Initialize the network object."""
262 """Refresh the state of the network."""
266 STATIONS_URI.format(uid=self.
network_idnetwork_id),
267 STATIONS_RESPONSE_SCHEMA,
269 self.
stationsstations = network[ATTR_NETWORK][ATTR_STATIONS_LIST]
270 self.
readyready.set()
271 except CityBikesRequestError
as err:
273 self.
readyready.clear()
275 raise PlatformNotReady
from err
279 """CityBikes API Sensor."""
281 _attr_attribution = CITYBIKES_ATTRIBUTION
282 _attr_native_unit_of_measurement =
"bikes"
283 _attr_icon =
"mdi:bike"
285 def __init__(self, network, station_id, entity_id):
286 """Initialize the sensor."""
292 """Update station state."""
293 for station
in self.
_network_network.stations:
294 if station[ATTR_ID] == self.
_station_id_station_id:
295 station_data = station
300 ATTR_UID: station_data.get(ATTR_EXTRA, {}).
get(ATTR_UID),
301 ATTR_LATITUDE: station_data.get(ATTR_LATITUDE),
302 ATTR_LONGITUDE: station_data.get(ATTR_LONGITUDE),
303 ATTR_EMPTY_SLOTS: station_data.get(ATTR_EMPTY_SLOTS),
304 ATTR_TIMESTAMP: station_data.get(ATTR_TIMESTAMP),
def __init__(self, hass, network_id)
def async_refresh(self, now=None)
def get_closest_network_id(self, latitude, longitude)
def __init__(self, network, station_id, entity_id)
_attr_extra_state_attributes
def async_citybikes_request(hass, uri, schema)
None async_setup_platform(HomeAssistant hass, ConfigType config, AddEntitiesCallback async_add_entities, DiscoveryInfoType|None discovery_info=None)
web.Response get(self, web.Request request, str config_key)
aiohttp.ClientSession async_get_clientsession(HomeAssistant hass, bool verify_ssl=True, socket.AddressFamily family=socket.AF_UNSPEC, ssl_util.SSLCipherList ssl_cipher=ssl_util.SSLCipherList.PYTHON_DEFAULT)
str async_generate_entity_id(str entity_id_format, str|None name, Iterable[str]|None current_ids=None, HomeAssistant|None hass=None)
CALLBACK_TYPE async_track_time_interval(HomeAssistant hass, Callable[[datetime], Coroutine[Any, Any, None]|None] action, timedelta interval, *str|None name=None, bool|None cancel_on_shutdown=None)