diff --git a/VERSIONS_WIP/99468 b/VERSIONS_WIP/99468
new file mode 100644
index 0000000000000000000000000000000000000000..6498ec3d41a1d514c6409f470578620d08549e1c
--- /dev/null
+++ b/VERSIONS_WIP/99468
@@ -0,0 +1 @@
+ - ticket #99468 : Open Data : Ajout de la représentation des ouvertures au format OpenStreetMap opening_hours dans la liste des bibliothèques en JSON
\ No newline at end of file
diff --git a/application/modules/opac/controllers/BibController.php b/application/modules/opac/controllers/BibController.php
index 6524a6972fb057d6cdd507c357e1411516c1b44f..9fdf4ef78c40472d13766b1a060db2e948e6a983 100644
--- a/application/modules/opac/controllers/BibController.php
+++ b/application/modules/opac/controllers/BibController.php
@@ -82,9 +82,10 @@ class BibController extends ZendAfi_Controller_Action {
     $openings = [];
     foreach($library->getOuvertures() as $ouverture)
       $openings[] = $ouverture->getRawAttributes();
-
     $fields['openings'] = $openings;
 
+    $fields['opening_hours'] = $library->getOpeningHours();
+
     $fields['latitude'] = '';
     $fields['longitude'] = '';
 
@@ -165,7 +166,7 @@ class BibController extends ZendAfi_Controller_Action {
   }
 
 
-  function mapviewAction() {
+  public function mapviewAction() {
     if (!$library = Class_Bib::find((int)$this->_request->getParam('id_bib')))
       return $this->_redirect('opac/bib/index');
 
@@ -310,6 +311,18 @@ class BibController extends ZendAfi_Controller_Action {
   }
 
 
+  public function openinghoursAction() {
+    if (!$library = Class_Bib::find((int)$this->_getParam('id')))
+      return $this->_redirect('opac/bib/index');
+
+    $this->_helper->getHelper('viewRenderer')->setNoRender(true);
+    if ($layout = Zend_Layout::getMvcInstance())
+      $layout->disableLayout();
+
+    echo $library->getOpeningHours();
+  }
+
+
   public function enLirePlusAction() {
     $this->_initLibrary();
   }
diff --git a/library/Class/Bib.php b/library/Class/Bib.php
index b3a82a12782df6679ec50c233917fbdd4ef51427..0f243d59ad022def6eac1f842cc60a04539e52ef 100644
--- a/library/Class/Bib.php
+++ b/library/Class/Bib.php
@@ -1024,4 +1024,9 @@ class Class_Bib extends Storm_Model_Abstract {
       ? USERFILESURL . static::BASE_PATH . $picture
       : BASE_URL . $picture;
   }
+
+
+  public function getOpeningHours() {
+    return (new Class_Bib_OpeningHours())->format($this);
+  }
 }
diff --git a/library/Class/Bib/OpeningHours.php b/library/Class/Bib/OpeningHours.php
new file mode 100644
index 0000000000000000000000000000000000000000..c52c3902225278567c21fcfd4df5d5a8b6f071c2
--- /dev/null
+++ b/library/Class/Bib/OpeningHours.php
@@ -0,0 +1,61 @@
+<?php
+/**
+ * Copyright (c) 2012-2019, Agence Française Informatique (AFI). All rights reserved.
+ *
+ * BOKEH 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.
+ *
+ * There are special exceptions to the terms and conditions of the AGPL as it
+ * is applied to this software (see README file).
+ *
+ * BOKEH 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 BOKEH; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
+ */
+
+
+class Class_Bib_OpeningHours {
+  protected $_specs;
+
+  public function format($library) {
+    if (!$library)
+      return '';
+
+    $this->_specs = new Storm_Collection();
+    $visitor = new Class_Ouverture_Visitor;
+    $library->acceptOpeningsVisitor($visitor);
+    if (!$visitor->hasOpenings())
+      return '';
+
+    $this->_collect($visitor->getDefault());
+
+    if ($library->isClosedOnHolidays())
+      $this->_specs->append('PH off');
+
+    foreach($visitor->getPeriodical() as $grouped)
+      $this->_collect($grouped);
+
+    $this->_collect($visitor->getExceptional())
+         ->_collect($visitor->getClosure());
+
+    return implode('; ', $this->_specs->getArrayCopy());
+  }
+
+
+  protected function _collect($openings) {
+    $specs = array_filter(array_map(function($each)
+                                    {
+                                      return $each->asOpeningHours();
+                                    },
+                                    $openings));
+    $this->_specs->addAll($specs);
+
+    return $this;
+  }
+}
diff --git a/library/Class/Ouverture.php b/library/Class/Ouverture.php
index f82aad082b2b537ff82173efee584e6a76130d61..a464754d0f962c601cc08350682abc3806ab513d 100644
--- a/library/Class/Ouverture.php
+++ b/library/Class/Ouverture.php
@@ -324,11 +324,21 @@ class Class_Ouverture extends Storm_Model_Abstract {
 
 
   public function isClosed() {
-    foreach(['debut_matin', 'fin_matin', 'debut_apres_midi', 'fin_apres_midi'] as $field)
-      if (!$this->getLoader()->isValueClosed($this->callGetterByAttributeName($field)))
-        return false;
+    return $this->isClosedAm() && $this->isClosedPm();
+  }
+
+
+  public function isClosedAm() {
+    $loader = $this->getLoader();
+    return $loader->isValueClosed($this->getDebutMatin())
+      && $loader->isValueClosed($this->getFinMatin());
+  }
 
-    return true;
+
+  public function isClosedPm() {
+    $loader = $this->getLoader();
+    return $loader->isValueClosed($this->getDebutApresMidi())
+      && $loader->isValueClosed($this->getFinApresMidi());
   }
 
 
@@ -338,4 +348,9 @@ class Class_Ouverture extends Storm_Model_Abstract {
 
     return (new static())->updateAttributes($attributes);
   }
+
+
+  public function asOpeningHours() {
+    return (new Class_Ouverture_OpeningHours)->format($this);
+  }
 }
\ No newline at end of file
diff --git a/library/Class/Ouverture/OpeningHours.php b/library/Class/Ouverture/OpeningHours.php
new file mode 100644
index 0000000000000000000000000000000000000000..b62aec537e24bfbf7a5c79cf148ef8d0c6cb48de
--- /dev/null
+++ b/library/Class/Ouverture/OpeningHours.php
@@ -0,0 +1,105 @@
+<?php
+/**
+ * Copyright (c) 2012-2019, Agence Française Informatique (AFI). All rights reserved.
+ *
+ * BOKEH 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.
+ *
+ * There are special exceptions to the terms and conditions of the AGPL as it
+ * is applied to this software (see README file).
+ *
+ * BOKEH 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 BOKEH; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
+ */
+
+
+/**
+ * I format a Class_Ouverture in OSM opening_hours tag format
+ *
+ * https://wiki.openstreetmap.org/wiki/Key:opening_hours
+ */
+class Class_Ouverture_OpeningHours {
+  protected $_opening;
+
+  public function __call($name, $args) {
+    if ($this->_opening)
+      return call_user_func_array([$this->_opening, $name], $args);
+
+    throw new RuntimeException('Call to unknown method Class_Ouverture_OpeningHours::' . $name);
+  }
+
+
+  public function format($opening) {
+    if (!$opening)
+      return '';
+
+    $this->_opening = $opening;
+
+    return $this->_day() . ' ' . $this->_times();
+  }
+
+
+  protected function _day() {
+    if ($this->isExceptional())
+      return $this->_date($this->getJour());
+
+    return $this->hasJourSemaine()
+      ? $this->_period() . $this->_weekday()
+      : '';
+  }
+
+
+  protected function _date($day) {
+    return date('Y M d', strtotime($day));
+  }
+
+
+  protected function _weekday() {
+    $map = [Class_Ouverture::LUNDI => 'Mo',
+            Class_Ouverture::MARDI => 'Tu',
+            Class_Ouverture::MERCREDI => 'We',
+            Class_Ouverture::JEUDI => 'Th',
+            Class_Ouverture::VENDREDI => 'Fr',
+            Class_Ouverture::SAMEDI => 'Sa',
+            Class_Ouverture::DIMANCHE => 'Su'];
+
+    $code = $this->getJourSemaine();
+
+    return isset($map[$code])
+      ? $map[$code]
+      : '';
+  }
+
+
+  protected function _period() {
+    if (!$this->hasValidityRange())
+      return '';
+
+    if ($this->hasValidityStart()
+        && $this->hasValidityEnd())
+      return $this->_date($this->getValidityStart())
+        . '-' . $this->_date($this->getValidityEnd())
+        . ' ';
+  }
+
+
+  protected function _times() {
+    $parts = [];
+    if (!$this->isClosedAm())
+      $parts[] = $this->getDebutMatin() . '-' . $this->getFinMatin();
+
+    if (!$this->isClosedPm())
+      $parts[] = $this->getDebutApresMidi() . '-' . $this->getFinApresMidi();
+
+    return $parts
+      ? implode(',', $parts)
+      : 'off';
+  }
+}
diff --git a/library/Class/Ouverture/Visitor.php b/library/Class/Ouverture/Visitor.php
index 237c6f4cd2146ce0c399551a07c8605e1b15ac26..c0b08314d04e2f204f639413b436b3b402f352ac 100644
--- a/library/Class/Ouverture/Visitor.php
+++ b/library/Class/Ouverture/Visitor.php
@@ -110,15 +110,15 @@ class Class_Ouverture_Visitor {
     if ($this->_should_keep_all)
       return true;
 
-    $now = $this->getCurrentTime();
-    $now_plus_30 = $this->_addToTime($now, 30);
+    $today_at_midnight = strtotime($this->getCurrentDate());
+    $today_plus_30_at_midnight = $this->_addToTime($today_at_midnight, 30);
 
     if ($opening->isExceptional()) {
       $opening_day = strtotime($opening->getJour());
-      return $now <= $opening_day && $now_plus_30 >= $opening_day;
+      return $today_at_midnight <= $opening_day && $today_plus_30_at_midnight >= $opening_day;
     }
 
-    return $opening->isValidDuring($now, $now_plus_30);
+    return $opening->isValidDuring($today_at_midnight, $today_plus_30_at_midnight);
   }
 
 
@@ -160,7 +160,7 @@ class Class_Ouverture_Visitor {
   protected function _getType($type) {
     return $this->_hasType($type)
       ? $this->_openings[$type]
-      : null;
+      : [];
   }
 
 
@@ -178,39 +178,6 @@ class Class_Ouverture_Visitor {
   }
 
 
-  protected function _renderTimeSegment($from, $to) {
-    if ($from == $to)
-      return $this->_renderHour($from);
-
-    $from = $this->_renderHour($from);
-    $to = $this->_renderHour($to);
-
-    return $from && $to
-      ? $from . ' - ' . $to
-      : '';
-  }
-
-
-  protected function _renderHour($hour) {
-    return '00:00' == $hour ? '' : str_replace(':', 'h', $hour);
-  }
-
-
-  protected function _renderTimes($am, $pm) {
-    if ('' != $am && '' != $pm) {
-      if (substr($am, -5, 5) == substr($pm, 0, 5))
-        return substr($am, 0, 5) . substr($pm, 5);
-
-      return $am . ' / ' . $pm;
-    }
-
-    if ($am)
-      return $am;
-
-    return $pm;
-  }
-
-
   protected function _escapeInfo($info) {
     return str_replace('BR', '<br />', urldecode(str_replace('%0D%0A','BR', $info)));
   }
diff --git a/tests/application/modules/opac/controllers/BibControllerIndexActionTest.php b/tests/application/modules/opac/controllers/BibControllerIndexActionTest.php
index 2c09c38674b25591496e52c798fd75c1c95283af..d1991cf11f02d1755fff8fd80ee974d82fefd3c8 100644
--- a/tests/application/modules/opac/controllers/BibControllerIndexActionTest.php
+++ b/tests/application/modules/opac/controllers/BibControllerIndexActionTest.php
@@ -218,9 +218,11 @@ class BibControllerIndexActionFormatJsonTest extends AbstractControllerTestCase
    * @test
    * @depends annecyShouldHave2CustomFields
    */
-  public function firstAnnecyFieldShouldBePublic($fields) {
-    $this->assertEquals('Public', $fields[0]->label);
-    $this->assertEquals('', $fields[0]->value);
+  public function annecyFieldPublicShouldBePresent($fields) {
+    $field = (new Storm_Collection($fields))
+      ->detect(function($item) { return 'Public' == $item->label; });
+    $this->assertNotNull($field);
+    $this->assertEquals('', $field->value);
   }
 
 
@@ -228,9 +230,11 @@ class BibControllerIndexActionFormatJsonTest extends AbstractControllerTestCase
    * @test
    * @depends annecyShouldHave2CustomFields
    */
-  public function secondAnnecyFieldShouldBeServices($fields) {
-    $this->assertEquals('Services', $fields[1]->label);
-    $this->assertEquals('Wifi;Restauration', $fields[1]->value);
+  public function annecyFieldServicesShouldBePresent($fields) {
+    $field = (new Storm_Collection($fields))
+      ->detect(function($item) { return 'Services' == $item->label; });
+    $this->assertNotNull($field);
+    $this->assertEquals('Wifi;Restauration', $field->value);
   }
 
 
@@ -238,4 +242,10 @@ class BibControllerIndexActionFormatJsonTest extends AbstractControllerTestCase
   public function annecyShouldBeOpenedTuesday() {
     $this->assertEquals('2', $this->_json[0]->openings[0]->jour_semaine);
   }
+
+
+  /** @test */
+  public function annecyShouldHaveOpeningHoursTuesday10to12ClosedOnPublicHolidays() {
+    $this->assertEquals('Tu 10:00-12:00; PH off', $this->_json[0]->opening_hours);
+  }
 }
diff --git a/tests/application/modules/opac/controllers/BibControllerTest.php b/tests/application/modules/opac/controllers/BibControllerTest.php
index 9c5b0a0b8c160207ac3770131dbfbeff8fc184a4..2e2cb69dfd4cf236e1c359f47b1971ef737aa003 100644
--- a/tests/application/modules/opac/controllers/BibControllerTest.php
+++ b/tests/application/modules/opac/controllers/BibControllerTest.php
@@ -936,6 +936,139 @@ class BibControllerBibViewAnnecyRangeOpeningsTest extends BibControllerLibraryWi
 
 
 
+class BibControllerOpeningHoursAnnecyTest extends BibControllerWithZoneTestCase {
+  public function setUp() {
+    parent::setUp();
+
+    Class_Ouverture_Visitor::setTimeSource(new TimeSourceForTest('2019-12-11 09:16:55'));
+
+    // open on mondays
+    $this->fixture('Class_Ouverture',
+                   ['id' => 33,
+                    'id_site' => 4,
+                    'jour_semaine' => Class_Ouverture::LUNDI,
+                    'debut_matin' => '00:00',
+                    'fin_matin' => '00:00',
+                    'debut_apres_midi' => '18:00',
+                    'fin_apres_midi' => '19:30',
+                    ]);
+
+    // open on tuesdays
+    $this->fixture('Class_Ouverture',
+                   ['id' => 34,
+                    'id_site' => 4,
+                    'jour_semaine' => Class_Ouverture::MARDI,
+                    'debut_matin' => '09:00',
+                    'fin_matin' => '12:00',
+                    'debut_apres_midi' => '13:00',
+                    'fin_apres_midi' => '19:30',
+                    ]);
+
+    // open on day
+    $this->fixture('Class_Ouverture',
+                   ['id' => 35,
+                    'id_site' => 4,
+                    'jour' => '2019-12-11',
+                    'debut_matin' => '09:00',
+                    'fin_matin' => '12:00',
+                    'debut_apres_midi' => '00:00',
+                    'fin_apres_midi' => '00:00',
+                    ]);
+
+    // open on period
+    $this->fixture('Class_Ouverture',
+                   ['id' => 36,
+                    'id_site' => 4,
+                    'jour_semaine' => Class_Ouverture::LUNDI,
+                    'debut_matin' => '09:00',
+                    'fin_matin' => '12:00',
+                    'debut_apres_midi' => '00:00',
+                    'fin_apres_midi' => '00:00',
+                    'validity_start' => '2019-12-01',
+                    'validity_end' => '2019-12-31'
+                    ]);
+
+    // closed on day
+    $this->fixture('Class_Ouverture',
+                   ['id' => 37,
+                    'id_site' => 4,
+                    'jour' => '2019-12-24',
+                    'debut_matin' => '00:00',
+                    'fin_matin' => '00:00',
+                    'debut_apres_midi' => '00:00',
+                    'fin_apres_midi' => '00:00',
+                    ]);
+
+    // closed on period
+    $this->fixture('Class_Ouverture',
+                   ['id' => 38,
+                    'id_site' => 4,
+                    'jour_semaine' => Class_Ouverture::LUNDI,
+                    'debut_matin' => '00:00',
+                    'fin_matin' => '00:00',
+                    'debut_apres_midi' => '00:00',
+                    'fin_apres_midi' => '00:00',
+                    'validity_start' => '2019-12-19',
+                    'validity_end' => '2019-12-23'
+                    ]);
+
+    Class_Bib::find(4)->setClosedOnHolidays(true)->assertSave();
+
+    $this->dispatch('bib/opening_hours/id/4', true);
+  }
+
+
+  public function tearDown() {
+    Class_Ouverture_Visitor::setTimeSource(null);
+    parent::tearDown();
+  }
+
+
+  /** @test */
+  public function shouldContainsEachMonday() {
+    $this->assertContains('Mo 18:00-19:30', $this->_response->getBody());
+  }
+
+
+  /** @test */
+  public function shouldContainsEachTuesday() {
+    $this->assertContains('Tu 09:00-12:00,13:00-19:30', $this->_response->getBody());
+  }
+
+
+  /** @test */
+  public function shouldContains2019_12_11() {
+    $this->assertContains('2019 Dec 11 09:00-12:00', $this->_response->getBody());
+  }
+
+
+  /** @test */
+  public function shouldContainsMondayFrom01_12To31_12() {
+    $this->assertContains('2019 Dec 01-2019 Dec 31 Mo 09:00-12:00', $this->_response->getBody());
+  }
+
+
+  /** @test */
+  public function shouldBeClosedOn2019_12_24() {
+    $this->assertContains('2019 Dec 24 off', $this->_response->getBody());
+  }
+
+
+  /** @test */
+  public function shouldBeClosedMondayFrom19_12To23_12() {
+    $this->assertContains('2019 Dec 19-2019 Dec 23 Mo off', $this->_response->getBody());
+  }
+
+
+  /** @test */
+  public function shouldBeClosedOnHolidays() {
+    $this->assertContains('PH off', $this->_response->getBody());
+  }
+}
+
+
+
+
 class BibControllerBibViewAnnecyWithOutdatedRangeOpeningsTest extends BibControllerBibViewTestCase {
   public function setUp() {
     parent::setUp();