We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Unique ID for entity/entities
Frankly, this is a minor / cosmetic structural issue, but its nice to have regarding the 2022.4 update, because it enables unique entites to be hidden (instead of on/off) - and it would be nice to have this on top of this already fantastic extension. 

**Context:** Setting up hacs_waste_collection_schedule creates an Entry in the default Dashboard (former Lovelace) called Calendar/Kalender with a single entry, in my case "AWB Köln". It shows only the on/off status. Its not showing the different countdowns till the pick-up dates for each type of trash.

**Expected Behaviour:** Upon Clicking the entry "AWB Köln" and further clicking "settings", I would be able to switch the entry to "hidden", thus making it invisible to default Dashboards.

**Current Behaviour:** Clicking "settings" reveals that the entity has no unique id and can't be set to hidden. 

**Tested solutions:** Manually adding a  "unique_id" the entry via configuration.yaml - throws an error and does not work. Either 
      unique_id: awbkoelnsource
      unique_id: calendar.awbkoelnsource
did not work.

Attached: Screenshots (in german) for explanation:
1:  Shown entry in dashboard (former lovelace)
2: Error when trying to access the entrys settings
3: Snippet of the configuration.yaml and the tried postiton for a unique_id

![Bildschirmfoto 2022-04-05 um 13 24 27](https://user-images.githubusercontent.com/2975047/161744234-55a693d5-5f58-4a48-9160-affd4f13823a.png)
![Bildschirmfoto 2022-04-05 um 13 24 37](https://user-images.githubusercontent.com/2975047/161744239-9ed44cb3-b997-4335-96db-9d15a7708568.png)
![Bildschirmfoto 2022-04-05 um 13 27 43](https://user-images.githubusercontent.com/2975047/161744245-9b9d3f3a-0f75-45ba-9caf-aac33681ef93.png)
 

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `custom_components/waste_collection_schedule/waste_collection_schedule/scraper.py`
Content:
```
1 #!/usr/bin/env python3
2 
3 import datetime
4 import importlib
5 import itertools
6 import logging
7 import traceback
8 from typing import Dict, List, Optional
9 
10 from .collection import Collection, CollectionGroup
11 
12 _LOGGER = logging.getLogger(__name__)
13 
14 
15 class Customize:
16     """Customize one waste collection type."""
17 
18     def __init__(self, waste_type, alias=None, show=True, icon=None, picture=None):
19         self._waste_type = waste_type
20         self._alias = alias
21         self._show = show
22         self._icon = icon
23         self._picture = picture
24 
25     @property
26     def waste_type(self):
27         return self._waste_type
28 
29     @property
30     def alias(self):
31         return self._alias
32 
33     @property
34     def show(self):
35         return self._show
36 
37     @property
38     def icon(self):
39         return self._icon
40 
41     @property
42     def picture(self):
43         return self._picture
44 
45     def __repr__(self):
46         return f"Customize{{waste_type={self._waste_type}, alias={self._alias}, show={self._show}, icon={self._icon}, picture={self._picture}}}"
47 
48 
49 def filter_function(entry: Collection, customize: Dict[str, Customize]):
50     c = customize.get(entry.type)
51     if c is None:
52         return True
53     else:
54         return c.show
55 
56 
57 def customize_function(entry: Collection, customize: Dict[str, Customize]):
58     c = customize.get(entry.type)
59     if c is not None:
60         if c.alias is not None:
61             entry.set_type(c.alias)
62         if c.icon is not None:
63             entry.set_icon(c.icon)
64         if c.picture is not None:
65             entry.set_picture(c.picture)
66     return entry
67 
68 
69 class Scraper:
70     def __init__(
71         self,
72         source,
73         customize: Dict[str, Customize],
74         title: str,
75         description: str,
76         url: Optional[str],
77         calendar_title: Optional[str],
78     ):
79         self._source = source
80         self._customize = customize
81         self._title = title
82         self._description = description
83         self._url = url
84         self._calendar_title = calendar_title
85         self._refreshtime = None
86         self._entries: List[Collection] = []
87 
88     @property
89     def source(self):
90         return self._source
91 
92     @property
93     def refreshtime(self):
94         return self._refreshtime
95 
96     @property
97     def title(self):
98         return self._title
99 
100     @property
101     def description(self):
102         return self._description
103 
104     @property
105     def url(self):
106         return self._url
107 
108     @property
109     def calendar_title(self):
110         return self._calendar_title or self._title
111 
112     def fetch(self):
113         """Fetch data from source."""
114         try:
115             # fetch returns a list of Collection's
116             entries = self._source.fetch()
117         except Exception:
118             _LOGGER.error(
119                 f"fetch failed for source {self._title}:\n{traceback.format_exc()}"
120             )
121             return
122         self._refreshtime = datetime.datetime.now()
123 
124         # strip whitespaces
125         for e in entries:
126             e.set_type(e.type.strip())
127 
128         # filter hidden entries
129         entries = filter(lambda x: filter_function(x, self._customize), entries)
130 
131         # customize fetched entries
132         entries = map(lambda x: customize_function(x, self._customize), entries)
133 
134         self._entries = list(entries)
135 
136     def get_types(self):
137         """Return set() of all collection types."""
138         types = set()
139         for e in self._entries:
140             types.add(e.type)
141         return types
142 
143     def get_upcoming(self, count=None, leadtime=None, types=None, include_today=False):
144         """Return list of all entries, limited by count and/or leadtime.
145 
146         Keyword arguments:
147         count -- limits the number of returned entries (default=10)
148         leadtime -- limits the timespan in days of returned entries (default=7, 0 = today)
149         """
150         return self._filter(
151             self._entries,
152             count=count,
153             leadtime=leadtime,
154             types=types,
155             include_today=include_today,
156         )
157 
158     def get_upcoming_group_by_day(
159         self, count=None, leadtime=None, types=None, include_today=False
160     ):
161         """Return list of all entries, grouped by day, limited by count and/or leadtime."""
162         entries = []
163 
164         iterator = itertools.groupby(
165             self._filter(
166                 self._entries,
167                 leadtime=leadtime,
168                 types=types,
169                 include_today=include_today,
170             ),
171             lambda e: e.date,
172         )
173 
174         for key, group in iterator:
175             entries.append(CollectionGroup.create(list(group)))
176         if count is not None:
177             entries = entries[:count]
178 
179         return entries
180 
181     def _filter(
182         self, entries, count=None, leadtime=None, types=None, include_today=False
183     ):
184         # remove unwanted waste types
185         if types is not None:
186             # generate set
187             types_set = {t for t in types}
188             entries = list(filter(lambda e: e.type in types_set, self._entries))
189 
190         # remove expired entries
191         now = datetime.datetime.now().date()
192         if include_today:
193             entries = list(filter(lambda e: e.date >= now, entries))
194         else:
195             entries = list(filter(lambda e: e.date > now, entries))
196 
197         # remove entries which are too far in the future (0 = today)
198         if leadtime is not None:
199             x = now + datetime.timedelta(days=leadtime)
200             entries = list(filter(lambda e: e.date <= x, entries))
201 
202         # ensure that entries are sorted by date
203         entries.sort(key=lambda e: e.date)
204 
205         # remove surplus entries
206         if count is not None:
207             entries = entries[:count]
208 
209         return entries
210 
211     @staticmethod
212     def create(
213         source_name: str,
214         customize: Dict[str, Customize],
215         source_args,
216         calendar_title: Optional[str] = None,
217     ):
218         # load source module
219 
220         # for home-assistant, use the last 3 folders, e.g. custom_component/wave_collection_schedule/waste_collection_schedule
221         # otherwise, only use waste_collection_schedule
222         try:
223             source_module = importlib.import_module(
224                 f"waste_collection_schedule.source.{source_name}"
225             )
226         except ImportError:
227             _LOGGER.error(f"source not found: {source_name}")
228             return
229 
230         # create source
231         source = source_module.Source(**source_args)  # type: ignore
232 
233         # create scraper
234         g = Scraper(
235             source=source,
236             customize=customize,
237             title=source_module.TITLE,  # type: ignore[attr-defined]
238             description=source_module.DESCRIPTION,  # type: ignore[attr-defined]
239             url=source_module.URL,  # type: ignore[attr-defined]
240             calendar_title=calendar_title,
241         )
242 
243         return g
244
```

Path: `custom_components/waste_collection_schedule/calendar.py`
Content:
```
1 """Calendar platform support for Waste Collection Schedule."""
2 
3 import logging
4 from datetime import timedelta
5 
6 from homeassistant.components.calendar import CalendarEventDevice
7 
8 _LOGGER = logging.getLogger(__name__)
9 
10 
11 async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
12     """Set up calendar platform."""
13     # We only want this platform to be set up via discovery.
14     if discovery_info is None:
15         return
16 
17     entities = []
18 
19     api = discovery_info["api"]
20 
21     for scraper in api.scrapers:
22         entities.append(WasteCollectionCalendar(api, scraper))
23 
24     async_add_entities(entities)
25 
26 
27 class WasteCollectionCalendar(CalendarEventDevice):
28     """Calendar entity class."""
29 
30     def __init__(self, api, scraper):
31         self._api = api
32         self._scraper = scraper
33 
34     @property
35     def name(self):
36         """Return entity name."""
37         return self._scraper.calendar_title
38 
39     @property
40     def event(self):
41         """Return next collection event."""
42         collections = self._scraper.get_upcoming(count=1, include_today=True)
43         if len(collections) == 0:
44             return None
45         else:
46             return self._convert(collections[0])
47 
48     async def async_get_events(self, hass, start_date, end_date):
49         """Return all events within specified time span."""
50         collections = []
51         for a in self._scraper.get_upcoming(include_today=True):
52             if a.date >= start_date.date() and a.date <= end_date.date():
53                 collections.append(self._convert(a))
54         return collections
55 
56     def _convert(self, collection):
57         """Convert an collection into a Home Assistant calendar event."""
58         return {
59             "uid": f"{self._scraper.calendar_title}-{collection.date.isoformat()}-{collection.type}",
60             "summary": collection.type,
61             "start": {"date": collection.date.isoformat()},
62             "end": {"date": (collection.date + timedelta(days=1)).isoformat()},
63             "allDay": True,
64         }
65
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/custom_components/waste_collection_schedule/calendar.py b/custom_components/waste_collection_schedule/calendar.py
--- a/custom_components/waste_collection_schedule/calendar.py
+++ b/custom_components/waste_collection_schedule/calendar.py
@@ -30,6 +30,7 @@
     def __init__(self, api, scraper):
         self._api = api
         self._scraper = scraper
+        self._attr_unique_id = scraper.unique_id + "_calendar"
 
     @property
     def name(self):
diff --git a/custom_components/waste_collection_schedule/waste_collection_schedule/scraper.py b/custom_components/waste_collection_schedule/waste_collection_schedule/scraper.py
--- a/custom_components/waste_collection_schedule/waste_collection_schedule/scraper.py
+++ b/custom_components/waste_collection_schedule/waste_collection_schedule/scraper.py
@@ -75,6 +75,7 @@
         description: str,
         url: Optional[str],
         calendar_title: Optional[str],
+        unique_id: str,
     ):
         self._source = source
         self._customize = customize
@@ -82,6 +83,7 @@
         self._description = description
         self._url = url
         self._calendar_title = calendar_title
+        self._unique_id = unique_id
         self._refreshtime = None
         self._entries: List[Collection] = []
 
@@ -109,6 +111,10 @@
     def calendar_title(self):
         return self._calendar_title or self._title
 
+    @property
+    def unique_id(self):
+        return self._unique_id
+
     def fetch(self):
         """Fetch data from source."""
         try:
@@ -238,6 +244,11 @@
             description=source_module.DESCRIPTION,  # type: ignore[attr-defined]
             url=source_module.URL,  # type: ignore[attr-defined]
             calendar_title=calendar_title,
+            unique_id=calc_unique_scraper_id(source_name, source_args),
         )
 
         return g
+
+
+def calc_unique_scraper_id(source_name, source_args):
+    return source_name + str(sorted(source_args.items()))

{"golden_diff": "diff --git a/custom_components/waste_collection_schedule/calendar.py b/custom_components/waste_collection_schedule/calendar.py\n--- a/custom_components/waste_collection_schedule/calendar.py\n+++ b/custom_components/waste_collection_schedule/calendar.py\n@@ -30,6 +30,7 @@\n     def __init__(self, api, scraper):\n         self._api = api\n         self._scraper = scraper\n+        self._attr_unique_id = scraper.unique_id + \"_calendar\"\n \n     @property\n     def name(self):\ndiff --git a/custom_components/waste_collection_schedule/waste_collection_schedule/scraper.py b/custom_components/waste_collection_schedule/waste_collection_schedule/scraper.py\n--- a/custom_components/waste_collection_schedule/waste_collection_schedule/scraper.py\n+++ b/custom_components/waste_collection_schedule/waste_collection_schedule/scraper.py\n@@ -75,6 +75,7 @@\n         description: str,\n         url: Optional[str],\n         calendar_title: Optional[str],\n+        unique_id: str,\n     ):\n         self._source = source\n         self._customize = customize\n@@ -82,6 +83,7 @@\n         self._description = description\n         self._url = url\n         self._calendar_title = calendar_title\n+        self._unique_id = unique_id\n         self._refreshtime = None\n         self._entries: List[Collection] = []\n \n@@ -109,6 +111,10 @@\n     def calendar_title(self):\n         return self._calendar_title or self._title\n \n+    @property\n+    def unique_id(self):\n+        return self._unique_id\n+\n     def fetch(self):\n         \"\"\"Fetch data from source.\"\"\"\n         try:\n@@ -238,6 +244,11 @@\n             description=source_module.DESCRIPTION,  # type: ignore[attr-defined]\n             url=source_module.URL,  # type: ignore[attr-defined]\n             calendar_title=calendar_title,\n+            unique_id=calc_unique_scraper_id(source_name, source_args),\n         )\n \n         return g\n+\n+\n+def calc_unique_scraper_id(source_name, source_args):\n+    return source_name + str(sorted(source_args.items()))\n", "issue": "Unique ID for entity/entities\nFrankly, this is a minor / cosmetic structural issue, but its nice to have regarding the 2022.4 update, because it enables unique entites to be hidden (instead of on/off) - and it would be nice to have this on top of this already fantastic extension. \r\n\r\n**Context:** Setting up hacs_waste_collection_schedule creates an Entry in the default Dashboard (former Lovelace) called Calendar/Kalender with a single entry, in my case \"AWB K\u00f6ln\". It shows only the on/off status. Its not showing the different countdowns till the pick-up dates for each type of trash.\r\n\r\n**Expected Behaviour:** Upon Clicking the entry \"AWB K\u00f6ln\" and further clicking \"settings\", I would be able to switch the entry to \"hidden\", thus making it invisible to default Dashboards.\r\n\r\n**Current Behaviour:** Clicking \"settings\" reveals that the entity has no unique id and can't be set to hidden. \r\n\r\n**Tested solutions:** Manually adding a  \"unique_id\" the entry via configuration.yaml - throws an error and does not work. Either \r\n      unique_id: awbkoelnsource\r\n      unique_id: calendar.awbkoelnsource\r\ndid not work.\r\n\r\nAttached: Screenshots (in german) for explanation:\r\n1:  Shown entry in dashboard (former lovelace)\r\n2: Error when trying to access the entrys settings\r\n3: Snippet of the configuration.yaml and the tried postiton for a unique_id\r\n\r\n![Bildschirmfoto 2022-04-05 um 13 24 27](https://user-images.githubusercontent.com/2975047/161744234-55a693d5-5f58-4a48-9160-affd4f13823a.png)\r\n![Bildschirmfoto 2022-04-05 um 13 24 37](https://user-images.githubusercontent.com/2975047/161744239-9ed44cb3-b997-4335-96db-9d15a7708568.png)\r\n![Bildschirmfoto 2022-04-05 um 13 27 43](https://user-images.githubusercontent.com/2975047/161744245-9b9d3f3a-0f75-45ba-9caf-aac33681ef93.png)\r\n \n", "before_files": [{"content": "#!/usr/bin/env python3\n\nimport datetime\nimport importlib\nimport itertools\nimport logging\nimport traceback\nfrom typing import Dict, List, Optional\n\nfrom .collection import Collection, CollectionGroup\n\n_LOGGER = logging.getLogger(__name__)\n\n\nclass Customize:\n    \"\"\"Customize one waste collection type.\"\"\"\n\n    def __init__(self, waste_type, alias=None, show=True, icon=None, picture=None):\n        self._waste_type = waste_type\n        self._alias = alias\n        self._show = show\n        self._icon = icon\n        self._picture = picture\n\n    @property\n    def waste_type(self):\n        return self._waste_type\n\n    @property\n    def alias(self):\n        return self._alias\n\n    @property\n    def show(self):\n        return self._show\n\n    @property\n    def icon(self):\n        return self._icon\n\n    @property\n    def picture(self):\n        return self._picture\n\n    def __repr__(self):\n        return f\"Customize{{waste_type={self._waste_type}, alias={self._alias}, show={self._show}, icon={self._icon}, picture={self._picture}}}\"\n\n\ndef filter_function(entry: Collection, customize: Dict[str, Customize]):\n    c = customize.get(entry.type)\n    if c is None:\n        return True\n    else:\n        return c.show\n\n\ndef customize_function(entry: Collection, customize: Dict[str, Customize]):\n    c = customize.get(entry.type)\n    if c is not None:\n        if c.alias is not None:\n            entry.set_type(c.alias)\n        if c.icon is not None:\n            entry.set_icon(c.icon)\n        if c.picture is not None:\n            entry.set_picture(c.picture)\n    return entry\n\n\nclass Scraper:\n    def __init__(\n        self,\n        source,\n        customize: Dict[str, Customize],\n        title: str,\n        description: str,\n        url: Optional[str],\n        calendar_title: Optional[str],\n    ):\n        self._source = source\n        self._customize = customize\n        self._title = title\n        self._description = description\n        self._url = url\n        self._calendar_title = calendar_title\n        self._refreshtime = None\n        self._entries: List[Collection] = []\n\n    @property\n    def source(self):\n        return self._source\n\n    @property\n    def refreshtime(self):\n        return self._refreshtime\n\n    @property\n    def title(self):\n        return self._title\n\n    @property\n    def description(self):\n        return self._description\n\n    @property\n    def url(self):\n        return self._url\n\n    @property\n    def calendar_title(self):\n        return self._calendar_title or self._title\n\n    def fetch(self):\n        \"\"\"Fetch data from source.\"\"\"\n        try:\n            # fetch returns a list of Collection's\n            entries = self._source.fetch()\n        except Exception:\n            _LOGGER.error(\n                f\"fetch failed for source {self._title}:\\n{traceback.format_exc()}\"\n            )\n            return\n        self._refreshtime = datetime.datetime.now()\n\n        # strip whitespaces\n        for e in entries:\n            e.set_type(e.type.strip())\n\n        # filter hidden entries\n        entries = filter(lambda x: filter_function(x, self._customize), entries)\n\n        # customize fetched entries\n        entries = map(lambda x: customize_function(x, self._customize), entries)\n\n        self._entries = list(entries)\n\n    def get_types(self):\n        \"\"\"Return set() of all collection types.\"\"\"\n        types = set()\n        for e in self._entries:\n            types.add(e.type)\n        return types\n\n    def get_upcoming(self, count=None, leadtime=None, types=None, include_today=False):\n        \"\"\"Return list of all entries, limited by count and/or leadtime.\n\n        Keyword arguments:\n        count -- limits the number of returned entries (default=10)\n        leadtime -- limits the timespan in days of returned entries (default=7, 0 = today)\n        \"\"\"\n        return self._filter(\n            self._entries,\n            count=count,\n            leadtime=leadtime,\n            types=types,\n            include_today=include_today,\n        )\n\n    def get_upcoming_group_by_day(\n        self, count=None, leadtime=None, types=None, include_today=False\n    ):\n        \"\"\"Return list of all entries, grouped by day, limited by count and/or leadtime.\"\"\"\n        entries = []\n\n        iterator = itertools.groupby(\n            self._filter(\n                self._entries,\n                leadtime=leadtime,\n                types=types,\n                include_today=include_today,\n            ),\n            lambda e: e.date,\n        )\n\n        for key, group in iterator:\n            entries.append(CollectionGroup.create(list(group)))\n        if count is not None:\n            entries = entries[:count]\n\n        return entries\n\n    def _filter(\n        self, entries, count=None, leadtime=None, types=None, include_today=False\n    ):\n        # remove unwanted waste types\n        if types is not None:\n            # generate set\n            types_set = {t for t in types}\n            entries = list(filter(lambda e: e.type in types_set, self._entries))\n\n        # remove expired entries\n        now = datetime.datetime.now().date()\n        if include_today:\n            entries = list(filter(lambda e: e.date >= now, entries))\n        else:\n            entries = list(filter(lambda e: e.date > now, entries))\n\n        # remove entries which are too far in the future (0 = today)\n        if leadtime is not None:\n            x = now + datetime.timedelta(days=leadtime)\n            entries = list(filter(lambda e: e.date <= x, entries))\n\n        # ensure that entries are sorted by date\n        entries.sort(key=lambda e: e.date)\n\n        # remove surplus entries\n        if count is not None:\n            entries = entries[:count]\n\n        return entries\n\n    @staticmethod\n    def create(\n        source_name: str,\n        customize: Dict[str, Customize],\n        source_args,\n        calendar_title: Optional[str] = None,\n    ):\n        # load source module\n\n        # for home-assistant, use the last 3 folders, e.g. custom_component/wave_collection_schedule/waste_collection_schedule\n        # otherwise, only use waste_collection_schedule\n        try:\n            source_module = importlib.import_module(\n                f\"waste_collection_schedule.source.{source_name}\"\n            )\n        except ImportError:\n            _LOGGER.error(f\"source not found: {source_name}\")\n            return\n\n        # create source\n        source = source_module.Source(**source_args)  # type: ignore\n\n        # create scraper\n        g = Scraper(\n            source=source,\n            customize=customize,\n            title=source_module.TITLE,  # type: ignore[attr-defined]\n            description=source_module.DESCRIPTION,  # type: ignore[attr-defined]\n            url=source_module.URL,  # type: ignore[attr-defined]\n            calendar_title=calendar_title,\n        )\n\n        return g\n", "path": "custom_components/waste_collection_schedule/waste_collection_schedule/scraper.py"}, {"content": "\"\"\"Calendar platform support for Waste Collection Schedule.\"\"\"\n\nimport logging\nfrom datetime import timedelta\n\nfrom homeassistant.components.calendar import CalendarEventDevice\n\n_LOGGER = logging.getLogger(__name__)\n\n\nasync def async_setup_platform(hass, config, async_add_entities, discovery_info=None):\n    \"\"\"Set up calendar platform.\"\"\"\n    # We only want this platform to be set up via discovery.\n    if discovery_info is None:\n        return\n\n    entities = []\n\n    api = discovery_info[\"api\"]\n\n    for scraper in api.scrapers:\n        entities.append(WasteCollectionCalendar(api, scraper))\n\n    async_add_entities(entities)\n\n\nclass WasteCollectionCalendar(CalendarEventDevice):\n    \"\"\"Calendar entity class.\"\"\"\n\n    def __init__(self, api, scraper):\n        self._api = api\n        self._scraper = scraper\n\n    @property\n    def name(self):\n        \"\"\"Return entity name.\"\"\"\n        return self._scraper.calendar_title\n\n    @property\n    def event(self):\n        \"\"\"Return next collection event.\"\"\"\n        collections = self._scraper.get_upcoming(count=1, include_today=True)\n        if len(collections) == 0:\n            return None\n        else:\n            return self._convert(collections[0])\n\n    async def async_get_events(self, hass, start_date, end_date):\n        \"\"\"Return all events within specified time span.\"\"\"\n        collections = []\n        for a in self._scraper.get_upcoming(include_today=True):\n            if a.date >= start_date.date() and a.date <= end_date.date():\n                collections.append(self._convert(a))\n        return collections\n\n    def _convert(self, collection):\n        \"\"\"Convert an collection into a Home Assistant calendar event.\"\"\"\n        return {\n            \"uid\": f\"{self._scraper.calendar_title}-{collection.date.isoformat()}-{collection.type}\",\n            \"summary\": collection.type,\n            \"start\": {\"date\": collection.date.isoformat()},\n            \"end\": {\"date\": (collection.date + timedelta(days=1)).isoformat()},\n            \"allDay\": True,\n        }\n", "path": "custom_components/waste_collection_schedule/calendar.py"}], "after_files": [{"content": "#!/usr/bin/env python3\n\nimport datetime\nimport importlib\nimport itertools\nimport logging\nimport traceback\nfrom typing import Dict, List, Optional\n\nfrom .collection import Collection, CollectionGroup\n\n_LOGGER = logging.getLogger(__name__)\n\n\nclass Customize:\n    \"\"\"Customize one waste collection type.\"\"\"\n\n    def __init__(self, waste_type, alias=None, show=True, icon=None, picture=None):\n        self._waste_type = waste_type\n        self._alias = alias\n        self._show = show\n        self._icon = icon\n        self._picture = picture\n\n    @property\n    def waste_type(self):\n        return self._waste_type\n\n    @property\n    def alias(self):\n        return self._alias\n\n    @property\n    def show(self):\n        return self._show\n\n    @property\n    def icon(self):\n        return self._icon\n\n    @property\n    def picture(self):\n        return self._picture\n\n    def __repr__(self):\n        return f\"Customize{{waste_type={self._waste_type}, alias={self._alias}, show={self._show}, icon={self._icon}, picture={self._picture}}}\"\n\n\ndef filter_function(entry: Collection, customize: Dict[str, Customize]):\n    c = customize.get(entry.type)\n    if c is None:\n        return True\n    else:\n        return c.show\n\n\ndef customize_function(entry: Collection, customize: Dict[str, Customize]):\n    c = customize.get(entry.type)\n    if c is not None:\n        if c.alias is not None:\n            entry.set_type(c.alias)\n        if c.icon is not None:\n            entry.set_icon(c.icon)\n        if c.picture is not None:\n            entry.set_picture(c.picture)\n    return entry\n\n\nclass Scraper:\n    def __init__(\n        self,\n        source,\n        customize: Dict[str, Customize],\n        title: str,\n        description: str,\n        url: Optional[str],\n        calendar_title: Optional[str],\n        unique_id: str,\n    ):\n        self._source = source\n        self._customize = customize\n        self._title = title\n        self._description = description\n        self._url = url\n        self._calendar_title = calendar_title\n        self._unique_id = unique_id\n        self._refreshtime = None\n        self._entries: List[Collection] = []\n\n    @property\n    def source(self):\n        return self._source\n\n    @property\n    def refreshtime(self):\n        return self._refreshtime\n\n    @property\n    def title(self):\n        return self._title\n\n    @property\n    def description(self):\n        return self._description\n\n    @property\n    def url(self):\n        return self._url\n\n    @property\n    def calendar_title(self):\n        return self._calendar_title or self._title\n\n    @property\n    def unique_id(self):\n        return self._unique_id\n\n    def fetch(self):\n        \"\"\"Fetch data from source.\"\"\"\n        try:\n            # fetch returns a list of Collection's\n            entries = self._source.fetch()\n        except Exception:\n            _LOGGER.error(\n                f\"fetch failed for source {self._title}:\\n{traceback.format_exc()}\"\n            )\n            return\n        self._refreshtime = datetime.datetime.now()\n\n        # strip whitespaces\n        for e in entries:\n            e.set_type(e.type.strip())\n\n        # filter hidden entries\n        entries = filter(lambda x: filter_function(x, self._customize), entries)\n\n        # customize fetched entries\n        entries = map(lambda x: customize_function(x, self._customize), entries)\n\n        self._entries = list(entries)\n\n    def get_types(self):\n        \"\"\"Return set() of all collection types.\"\"\"\n        types = set()\n        for e in self._entries:\n            types.add(e.type)\n        return types\n\n    def get_upcoming(self, count=None, leadtime=None, types=None, include_today=False):\n        \"\"\"Return list of all entries, limited by count and/or leadtime.\n\n        Keyword arguments:\n        count -- limits the number of returned entries (default=10)\n        leadtime -- limits the timespan in days of returned entries (default=7, 0 = today)\n        \"\"\"\n        return self._filter(\n            self._entries,\n            count=count,\n            leadtime=leadtime,\n            types=types,\n            include_today=include_today,\n        )\n\n    def get_upcoming_group_by_day(\n        self, count=None, leadtime=None, types=None, include_today=False\n    ):\n        \"\"\"Return list of all entries, grouped by day, limited by count and/or leadtime.\"\"\"\n        entries = []\n\n        iterator = itertools.groupby(\n            self._filter(\n                self._entries,\n                leadtime=leadtime,\n                types=types,\n                include_today=include_today,\n            ),\n            lambda e: e.date,\n        )\n\n        for key, group in iterator:\n            entries.append(CollectionGroup.create(list(group)))\n        if count is not None:\n            entries = entries[:count]\n\n        return entries\n\n    def _filter(\n        self, entries, count=None, leadtime=None, types=None, include_today=False\n    ):\n        # remove unwanted waste types\n        if types is not None:\n            # generate set\n            types_set = {t for t in types}\n            entries = list(filter(lambda e: e.type in types_set, self._entries))\n\n        # remove expired entries\n        now = datetime.datetime.now().date()\n        if include_today:\n            entries = list(filter(lambda e: e.date >= now, entries))\n        else:\n            entries = list(filter(lambda e: e.date > now, entries))\n\n        # remove entries which are too far in the future (0 = today)\n        if leadtime is not None:\n            x = now + datetime.timedelta(days=leadtime)\n            entries = list(filter(lambda e: e.date <= x, entries))\n\n        # ensure that entries are sorted by date\n        entries.sort(key=lambda e: e.date)\n\n        # remove surplus entries\n        if count is not None:\n            entries = entries[:count]\n\n        return entries\n\n    @staticmethod\n    def create(\n        source_name: str,\n        customize: Dict[str, Customize],\n        source_args,\n        calendar_title: Optional[str] = None,\n    ):\n        # load source module\n\n        # for home-assistant, use the last 3 folders, e.g. custom_component/wave_collection_schedule/waste_collection_schedule\n        # otherwise, only use waste_collection_schedule\n        try:\n            source_module = importlib.import_module(\n                f\"waste_collection_schedule.source.{source_name}\"\n            )\n        except ImportError:\n            _LOGGER.error(f\"source not found: {source_name}\")\n            return\n\n        # create source\n        source = source_module.Source(**source_args)  # type: ignore\n\n        # create scraper\n        g = Scraper(\n            source=source,\n            customize=customize,\n            title=source_module.TITLE,  # type: ignore[attr-defined]\n            description=source_module.DESCRIPTION,  # type: ignore[attr-defined]\n            url=source_module.URL,  # type: ignore[attr-defined]\n            calendar_title=calendar_title,\n            unique_id=calc_unique_scraper_id(source_name, source_args),\n        )\n\n        return g\n\n\ndef calc_unique_scraper_id(source_name, source_args):\n    return source_name + str(sorted(source_args.items()))\n", "path": "custom_components/waste_collection_schedule/waste_collection_schedule/scraper.py"}, {"content": "\"\"\"Calendar platform support for Waste Collection Schedule.\"\"\"\n\nimport logging\nfrom datetime import timedelta\n\nfrom homeassistant.components.calendar import CalendarEventDevice\n\n_LOGGER = logging.getLogger(__name__)\n\n\nasync def async_setup_platform(hass, config, async_add_entities, discovery_info=None):\n    \"\"\"Set up calendar platform.\"\"\"\n    # We only want this platform to be set up via discovery.\n    if discovery_info is None:\n        return\n\n    entities = []\n\n    api = discovery_info[\"api\"]\n\n    for scraper in api.scrapers:\n        entities.append(WasteCollectionCalendar(api, scraper))\n\n    async_add_entities(entities)\n\n\nclass WasteCollectionCalendar(CalendarEventDevice):\n    \"\"\"Calendar entity class.\"\"\"\n\n    def __init__(self, api, scraper):\n        self._api = api\n        self._scraper = scraper\n        self._attr_unique_id = scraper.unique_id + \"_calendar\"\n\n    @property\n    def name(self):\n        \"\"\"Return entity name.\"\"\"\n        return self._scraper.calendar_title\n\n    @property\n    def event(self):\n        \"\"\"Return next collection event.\"\"\"\n        collections = self._scraper.get_upcoming(count=1, include_today=True)\n        if len(collections) == 0:\n            return None\n        else:\n            return self._convert(collections[0])\n\n    async def async_get_events(self, hass, start_date, end_date):\n        \"\"\"Return all events within specified time span.\"\"\"\n        collections = []\n        for a in self._scraper.get_upcoming(include_today=True):\n            if a.date >= start_date.date() and a.date <= end_date.date():\n                collections.append(self._convert(a))\n        return collections\n\n    def _convert(self, collection):\n        \"\"\"Convert an collection into a Home Assistant calendar event.\"\"\"\n        return {\n            \"uid\": f\"{self._scraper.calendar_title}-{collection.date.isoformat()}-{collection.type}\",\n            \"summary\": collection.type,\n            \"start\": {\"date\": collection.date.isoformat()},\n            \"end\": {\"date\": (collection.date + timedelta(days=1)).isoformat()},\n            \"allDay\": True,\n        }\n", "path": "custom_components/waste_collection_schedule/calendar.py"}]}