diff --git a/satpy/etc/readers/msi_safe.yaml b/satpy/etc/readers/msi_safe.yaml
index c83b560539..20b324a036 100644
--- a/satpy/etc/readers/msi_safe.yaml
+++ b/satpy/etc/readers/msi_safe.yaml
@@ -1,7 +1,7 @@
reader:
name: msi_safe
short_name: MSI SAFE
- long_name: Sentinel-2 A and B MSI data in SAFE format
+ long_name: Sentinel-2 A and B MSI data in SAFE format, supporting L1C format only.
description: SAFE Reader for MSI data (Sentinel-2)
status: Nominal
supports_fsspec: false
@@ -10,16 +10,16 @@ reader:
reader: !!python/name:satpy.readers.yaml_reader.FileYAMLReader
file_types:
- safe_granule:
+ safe_granule_l1c:
file_reader: !!python/name:satpy.readers.msi_safe.SAFEMSIL1C
- file_patterns: ['{fmission_id:3s}_MSIL1C_{observation_time:%Y%m%dT%H%M%S}_N{fprocessing_baseline_number:4d}_R{relative_orbit_number:3d}_T{dtile_number:5s}_{dproduct_discriminator:%Y%m%dT%H%M%S}.SAFE/GRANULE/L1C_T{gtile_number:5s}_A{absolute_orbit_number:6d}_{gfile_discriminator:%Y%m%dT%H%M%S}/IMG_DATA/T{tile_number:5s}_{file_discriminator:%Y%m%dT%H%M%S}_{band_name:3s}.jp2']
+ file_patterns: ['{fmission_id:3s}_MSI{proclevel:3s}_{observation_time:%Y%m%dT%H%M%S}_N{fprocessing_baseline_number:4d}_R{relative_orbit_number:3d}_T{dtile_number:5s}_{dproduct_discriminator:%Y%m%dT%H%M%S}.SAFE/GRANULE/L1C_T{gtile_number:5s}_A{absolute_orbit_number:6d}_{gfile_discriminator:%Y%m%dT%H%M%S}/IMG_DATA/T{tile_number:5s}_{file_discriminator:%Y%m%dT%H%M%S}_{band_name:3s}.jp2']
requires: [safe_metadata, safe_tile_metadata]
safe_tile_metadata:
file_reader: !!python/name:satpy.readers.msi_safe.SAFEMSITileMDXML
- file_patterns: ['{fmission_id:3s}_MSIL1C_{observation_time:%Y%m%dT%H%M%S}_N{fprocessing_baseline_number:4d}_R{relative_orbit_number:3d}_T{dtile_number:5s}_{dproduct_discriminator:%Y%m%dT%H%M%S}.SAFE/GRANULE/L1C_T{gtile_number:5s}_A{absolute_orbit_number:6d}_{gfile_discriminator:%Y%m%dT%H%M%S}/MTD_TL.xml']
+ file_patterns: ['{fmission_id:3s}_MSI{proclevel:3s}_{observation_time:%Y%m%dT%H%M%S}_N{fprocessing_baseline_number:4d}_R{relative_orbit_number:3d}_T{dtile_number:5s}_{dproduct_discriminator:%Y%m%dT%H%M%S}.SAFE/GRANULE/L1C_T{gtile_number:5s}_A{absolute_orbit_number:6d}_{gfile_discriminator:%Y%m%dT%H%M%S}/MTD_TL.xml']
safe_metadata:
file_reader: !!python/name:satpy.readers.msi_safe.SAFEMSIMDXML
- file_patterns: ['{fmission_id:3s}_MSIL1C_{observation_time:%Y%m%dT%H%M%S}_N{fprocessing_baseline_number:4d}_R{relative_orbit_number:3d}_T{dtile_number:5s}_{dproduct_discriminator:%Y%m%dT%H%M%S}.SAFE/MTD_MSIL1C.xml']
+ file_patterns: ['{fmission_id:3s}_MSI{proclevel:3s}_{observation_time:%Y%m%dT%H%M%S}_N{fprocessing_baseline_number:4d}_R{relative_orbit_number:3d}_T{dtile_number:5s}_{dproduct_discriminator:%Y%m%dT%H%M%S}.SAFE/MTD_MSIL1C.xml']
datasets:
@@ -39,7 +39,7 @@ datasets:
counts:
standard_name: counts
units: "1"
- file_type: safe_granule
+ file_type: safe_granule_l1c
B02:
name: B02
@@ -56,7 +56,7 @@ datasets:
counts:
standard_name: counts
units: "1"
- file_type: safe_granule
+ file_type: safe_granule_l1c
B03:
name: B03
@@ -73,7 +73,7 @@ datasets:
counts:
standard_name: counts
units: "1"
- file_type: safe_granule
+ file_type: safe_granule_l1c
B04:
name: B04
@@ -90,7 +90,7 @@ datasets:
counts:
standard_name: counts
units: "1"
- file_type: safe_granule
+ file_type: safe_granule_l1c
B05:
name: B05
@@ -107,7 +107,7 @@ datasets:
counts:
standard_name: counts
units: "1"
- file_type: safe_granule
+ file_type: safe_granule_l1c
B06:
name: B06
@@ -124,7 +124,7 @@ datasets:
counts:
standard_name: counts
units: "1"
- file_type: safe_granule
+ file_type: safe_granule_l1c
B07:
name: B07
@@ -141,7 +141,7 @@ datasets:
counts:
standard_name: counts
units: "1"
- file_type: safe_granule
+ file_type: safe_granule_l1c
B08:
name: B08
@@ -158,7 +158,7 @@ datasets:
counts:
standard_name: counts
units: "1"
- file_type: safe_granule
+ file_type: safe_granule_l1c
B8A:
name: B8A
@@ -175,7 +175,7 @@ datasets:
counts:
standard_name: counts
units: "1"
- file_type: safe_granule
+ file_type: safe_granule_l1c
B09:
name: B09
@@ -192,7 +192,7 @@ datasets:
counts:
standard_name: counts
units: "1"
- file_type: safe_granule
+ file_type: safe_granule_l1c
B10:
name: B10
@@ -209,7 +209,7 @@ datasets:
counts:
standard_name: counts
units: "1"
- file_type: safe_granule
+ file_type: safe_granule_l1c
B11:
name: B11
@@ -226,7 +226,7 @@ datasets:
counts:
standard_name: counts
units: "1"
- file_type: safe_granule
+ file_type: safe_granule_l1c
B12:
name: B12
@@ -243,7 +243,7 @@ datasets:
counts:
standard_name: counts
units: "1"
- file_type: safe_granule
+ file_type: safe_granule_l1c
solar_zenith_angle:
diff --git a/satpy/readers/msi_safe.py b/satpy/readers/msi_safe.py
index ec17a98872..5ec5ff3ea0 100644
--- a/satpy/readers/msi_safe.py
+++ b/satpy/readers/msi_safe.py
@@ -28,13 +28,14 @@
reader_kwargs={'mask_saturated': False})
scene.load(['B01'])
-L1B format description for the files read here:
+L1C format description for the files read here:
https://sentinels.copernicus.eu/documents/247904/0/Sentinel-2-product-specifications-document-V14-9.pdf/
"""
import logging
+from datetime import datetime
import dask.array as da
import defusedxml.ElementTree as ET
@@ -63,13 +64,14 @@ def __init__(self, filename, filename_info, filetype_info, mda, tile_mda, mask_s
super(SAFEMSIL1C, self).__init__(filename, filename_info,
filetype_info)
del mask_saturated
- self._start_time = filename_info["observation_time"]
- self._end_time = filename_info["observation_time"]
self._channel = filename_info["band_name"]
self._tile_mda = tile_mda
self._mda = mda
self.platform_name = PLATFORMS[filename_info["fmission_id"]]
+ self._start_time = self._tile_mda.start_time()
+ self._end_time = filename_info["observation_time"]
+
def get_dataset(self, key, info):
"""Load a dataset."""
if self._channel != key["name"]:
@@ -269,6 +271,11 @@ def _shape(self, resolution):
cols = int(self.geocoding.find('Size[@resolution="' + str(resolution) + '"]/NCOLS').text)
return cols, rows
+ def start_time(self):
+ """Get the observation time from the tile metadata."""
+ timestr = self.root.find(".//SENSING_TIME").text
+ return datetime.strptime(timestr, "%Y-%m-%dT%H:%M:%S.%fZ")
+
@staticmethod
def _do_interp(minterp, xcoord, ycoord):
interp_points2 = np.vstack((ycoord.ravel(), xcoord.ravel()))
diff --git a/satpy/tests/reader_tests/test_msi_safe.py b/satpy/tests/reader_tests/test_msi_safe.py
index 0255aac085..b919278bf5 100644
--- a/satpy/tests/reader_tests/test_msi_safe.py
+++ b/satpy/tests/reader_tests/test_msi_safe.py
@@ -17,6 +17,7 @@
# satpy. If not, see .
"""Module for testing the satpy.readers.msi_safe module."""
import unittest.mock as mock
+from datetime import datetime
from io import BytesIO, StringIO
import numpy as np
@@ -25,6 +26,10 @@
from satpy.tests.utils import make_dataid
+# Datetimes used for checking start time is correctly set.
+fname_dt = datetime(2020, 10, 1, 18, 35, 41)
+tilemd_dt = datetime(2020, 10, 1, 16, 34, 23, 153611)
+
mtd_tile_xml = b"""
@@ -873,6 +878,10 @@ def setup_method(self):
self.old_xml_fh = SAFEMSIMDXML(StringIO(mtd_l1c_old_xml), filename_info, mock.MagicMock())
self.xml_fh = SAFEMSIMDXML(StringIO(mtd_l1c_xml), filename_info, mock.MagicMock(), mask_saturated=True)
+ def test_start_time(self):
+ """Ensure start time is read correctly from XML."""
+ assert self.xml_tile_fh.start_time() == tilemd_dt
+
def test_satellite_zenith_array(self):
"""Test reading the satellite zenith array."""
info = dict(xml_tag="Viewing_Incidence_Angles_Grids", xml_item="Zenith")
@@ -980,10 +989,11 @@ class TestSAFEMSIL1C:
def setup_method(self):
"""Set up the test."""
from satpy.readers.msi_safe import SAFEMSITileMDXML
- self.filename_info = dict(observation_time=None, fmission_id="S2A", band_name="B01", dtile_number=None)
+ self.filename_info = dict(observation_time=fname_dt, fmission_id="S2A", band_name="B01", dtile_number=None)
self.fake_data = xr.Dataset({"band_data": xr.DataArray([[[0, 1], [65534, 65535]]], dims=["band", "x", "y"])})
self.tile_mda = mock.create_autospec(SAFEMSITileMDXML)(BytesIO(mtd_tile_xml),
self.filename_info, mock.MagicMock())
+ self.tile_mda.start_time.return_value = tilemd_dt
@pytest.mark.parametrize(("mask_saturated", "calibration", "expected"),
[(True, "reflectance", [[np.nan, 0.01 - 10], [645.34, np.inf]]),
@@ -1001,3 +1011,13 @@ def test_calibration_and_masking(self, mask_saturated, calibration, expected):
with mock.patch("xarray.open_dataset", return_value=self.fake_data):
res = self.jp2_fh.get_dataset(make_dataid(name="B01", calibration=calibration), info=dict())
np.testing.assert_allclose(res, expected)
+
+
+ def test_start_time(self):
+ """Test that the correct start time is returned."""
+ from satpy.readers.msi_safe import SAFEMSIL1C, SAFEMSIMDXML
+
+ mda = SAFEMSIMDXML(StringIO(mtd_l1c_xml), self.filename_info, mock.MagicMock())
+ self.jp2_fh = SAFEMSIL1C("somefile", self.filename_info, mock.MagicMock(),
+ mda, self.tile_mda)
+ assert tilemd_dt == self.jp2_fh.start_time