diff --git a/modules/velib/__init__.py b/modules/velib/__init__.py
new file mode 100644
index 00000000..be50b34c
--- /dev/null
+++ b/modules/velib/__init__.py
@@ -0,0 +1,24 @@
+# -*- coding: utf-8 -*-
+
+# Copyright(C) 2013 dud
+#
+# This file is part of weboob.
+#
+# weboob is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# weboob is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with weboob. If not, see .
+
+
+from .backend import VelibBackend
+
+
+__all__ = ['VelibBackend']
diff --git a/modules/velib/backend.py b/modules/velib/backend.py
new file mode 100644
index 00000000..a046538c
--- /dev/null
+++ b/modules/velib/backend.py
@@ -0,0 +1,93 @@
+# -*- coding: utf-8 -*-
+
+# Copyright(C) 2013 dud
+#
+# This file is part of weboob.
+#
+# weboob is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# weboob is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with weboob. If not, see .
+
+
+from weboob.tools.backend import BaseBackend
+from weboob.capabilities.gauge import ICapGauge, GaugeSensor, Gauge, SensorNotFound
+
+from .browser import VelibBrowser
+
+import re
+
+__all__ = ['VelibBackend']
+
+
+class VelibBackend(BaseBackend, ICapGauge):
+ NAME = 'velib'
+ DESCRIPTION = u'get Vélib\' information'
+ MAINTAINER = u'Herve Werner'
+ EMAIL = 'dud225@hotmail.com'
+ VERSION = '0.g'
+ LICENSE = 'AGPLv3'
+
+ BROWSER = VelibBrowser
+ STORAGE = {'boards' : {}}
+
+
+ def iter_gauges(self, pattern=None):
+ if pattern is None:
+ for gauge in self.browser.get_station_list():
+ yield gauge
+ else:
+ lowpattern = pattern.lower()
+ for gauge in self.browser.get_station_list():
+ if lowpattern in gauge.name.lower() or lowpattern in gauge.city.lower():
+ yield gauge
+
+ def iter_sensors(self, gauge, pattern=None):
+ if not isinstance(gauge, Gauge):
+ gauge = self._get_gauge_by_id(gauge)
+ if gauge is None:
+ raise SensorNotFound()
+
+ gauge.sensors = self.browser.get_station_infos(gauge)
+ if pattern is None:
+ for sensor in gauge.sensors:
+ yield sensor
+ else:
+ lowpattern = pattern.lower()
+ for sensor in gauge.sensors:
+ if lowpattern in sensor.name.lower():
+ yield sensor
+
+ def get_last_measure(self, sensor):
+ if not isinstance(sensor, GaugeSensor):
+ sensor = self._get_sensor_by_id(sensor)
+ if sensor is None:
+ raise SensorNotFound()
+ return sensor.lastvalue
+
+ def _get_gauge_by_id(self, id):
+ for gauge in self.browser.get_station_list():
+ if id == gauge.id:
+ return gauge
+ return None
+
+ def _get_sensor_by_id(self, id):
+ reSensorId = re.search ('(\d+)-((bikes|attach|status))', id, re.IGNORECASE)
+ if reSensorId:
+ gauge = reSensorId.group(1)
+ pattern = reSensorId.group(2)
+ sensor_generator = self.iter_sensors(gauge, pattern)
+ if sensor_generator:
+ return next(sensor_generator)
+ else:
+ return None
+ else:
+ return None
diff --git a/modules/velib/browser.py b/modules/velib/browser.py
new file mode 100644
index 00000000..1e7fe792
--- /dev/null
+++ b/modules/velib/browser.py
@@ -0,0 +1,47 @@
+# -*- coding: utf-8 -*-
+
+# Copyright(C) 2013 dud
+#
+# This file is part of weboob.
+#
+# weboob is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# weboob is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with weboob. If not, see .
+
+
+from weboob.tools.browser import BaseBrowser
+
+from .pages import ListStationsPage, InfoStationPage
+
+
+__all__ = ['VelibBrowser']
+
+
+class VelibBrowser(BaseBrowser):
+ PROTOCOL = 'http'
+ DOMAIN = 'www.velib.paris.fr/service'
+ ENCODING = None
+
+ PAGES = {
+ '%s://%s/stationdetails/paris/.*' % (PROTOCOL, DOMAIN): InfoStationPage,
+ '%s://%s/carto' % (PROTOCOL, DOMAIN): ListStationsPage,
+ }
+
+ def get_station_list(self):
+ if not self.is_on_page(ListStationsPage):
+ self.location(u'%s://%s/carto' % (self.PROTOCOL, self.DOMAIN))
+ return self.page.get_station_list()
+
+ def get_station_infos(self, gauge):
+ self.location('%s://%s/stationdetails/paris/%s' % (self.PROTOCOL, self.DOMAIN, gauge.id))
+ return self.page.get_station_infos(gauge.id)
+
diff --git a/modules/velib/pages.py b/modules/velib/pages.py
new file mode 100644
index 00000000..1e900df4
--- /dev/null
+++ b/modules/velib/pages.py
@@ -0,0 +1,107 @@
+# -*- coding: utf-8 -*-
+
+# Copyright(C) 2013 dud
+#
+# This file is part of weboob.
+#
+# weboob is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# weboob is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with weboob. If not, see .
+
+
+from weboob.tools.browser import BasePage
+from weboob.capabilities.gauge import Gauge, GaugeMeasure, GaugeSensor
+from weboob.capabilities.base import NotLoaded
+import datetime
+import re
+
+__all__ = ['InfoStationPage', 'ListStationsPage']
+
+AdresseStation = {}
+
+class InfoStationPage(BasePage):
+ def _create_bikes_sensor(self, value, gauge_id, last_update, adresse):
+ levelbikes = GaugeSensor(gauge_id + '-bikes')
+ levelbikes.name = u'Bikes'
+ levelbikes.address = u'%s' % adresse
+ lastvalue = GaugeMeasure()
+ lastvalue.level = float(value)
+ lastvalue.date = last_update
+ if lastvalue.level < 1:
+ lastvalue.alarm = u'Empty station'
+ levelbikes.lastvalue = lastvalue
+ levelbikes.history = NotLoaded
+ levelbikes.gaugeid = gauge_id
+ return levelbikes
+
+ def _create_attach_sensor(self, value, gauge_id, last_update, adresse):
+ levelattach = GaugeSensor(gauge_id + '-attach')
+ levelattach.name = u'Attach'
+ levelattach.address = u'%s' % adresse
+ lastvalue = GaugeMeasure()
+ if lastvalue.level < 1:
+ lastvalue.alarm = u'Full station'
+ lastvalue.level = float(value)
+ lastvalue.date = last_update
+ levelattach.lastvalue = lastvalue
+ levelattach.history = NotLoaded
+ levelattach.gaugeid = gauge_id
+ return levelattach
+
+ def _create_status_sensor(self, value, gauge_id, last_update, adresse):
+ levelstatus = GaugeSensor(gauge_id + '-status')
+ levelstatus.name = u'Status'
+ levelstatus.address = u'%s' % adresse
+ lastvalue = GaugeMeasure()
+ status = float(value)
+ if status == 0:
+ status = 1
+ else:
+ status = -1
+ if lastvalue.level < 1:
+ lastvalue.alarm = u'Not available station'
+ lastvalue.level = float(status)
+ lastvalue.date = last_update
+ levelstatus.lastvalue = lastvalue
+ levelstatus.history = NotLoaded
+ levelstatus.gaugeid = gauge_id
+ return levelstatus
+
+ def _get_last_update(self, last_update):
+ return datetime.datetime.now() - datetime.timedelta(seconds=int(re.match(r'\d+', last_update).group(0)))
+
+ def get_station_infos(self, gauge_id):
+ sensors = []
+
+ last_update = datetime.datetime.fromtimestamp(float(self.parser.select(self.document.getroot(), 'updated', 1).text))
+ adresse = AdresseStation[gauge_id]
+
+ sensors.append(self._create_bikes_sensor(self.parser.select(self.document.getroot(), 'available', 1).text, gauge_id, last_update, adresse))
+ sensors.append(self._create_attach_sensor(self.parser.select(self.document.getroot(), 'free', 1).text, gauge_id, last_update, adresse))
+ sensors.append(self._create_status_sensor(self.parser.select(self.document.getroot(), 'open', 1).text, gauge_id, last_update, adresse))
+
+ return sensors
+
+
+class ListStationsPage(BasePage):
+ def get_station_list(self):
+ gauges = []
+ for marker in self.parser.select(self.document.getroot(), 'marker'):
+ gauge = Gauge(int(marker.get('number')))
+ gauge.name = unicode(marker.get('address')).rsplit('-', 1)[0]
+ full_address = re.search(r'\d\d\d\d\d.*', unicode(marker.get('fulladdress')))
+ if full_address:
+ gauge.city = full_address.group()
+ gauge.object = u'velib'
+ gauges.append(gauge)
+ AdresseStation[marker.get('number')] = marker.get('fulladdress')
+ return gauges
diff --git a/modules/velib/test.py b/modules/velib/test.py
new file mode 100644
index 00000000..f12bc569
--- /dev/null
+++ b/modules/velib/test.py
@@ -0,0 +1,35 @@
+# -*- coding: utf-8 -*-
+
+# Copyright(C) 2013 dud
+#
+# This file is part of weboob.
+#
+# weboob is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# weboob is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with weboob. If not, see .
+
+from weboob.tools.test import BackendTest
+
+
+class VelibTest(BackendTest):
+ BACKEND = 'velib'
+
+ def test_velib(self):
+ l = list(self.backend.iter_gauges())
+ self.assertTrue(len(l) > 0)
+
+ gauge = l[0]
+ s = list(self.backend.iter_sensors(gauge))
+ self.assertTrue(len(s) > 0)
+
+ sensor = s[0]
+ self.assertTrue(self.backend.get_last_measure(sensor.id) is not None)
diff --git a/modules/velib/velib.png b/modules/velib/velib.png
new file mode 100644
index 00000000..724e339c
Binary files /dev/null and b/modules/velib/velib.png differ