1616from homeassistant .helpers .entity_platform import AddEntitiesCallback
1717from .const import DOMAIN , ICON_UPDATE , ICON_CREDIT , ICON_NO_LIMIT , ICON_FREE_EXPRESS , ICON_DELIVERY , ICON_BAGS , \
1818 ICON_CART , ICON_ACCOUNT , ICON_EMAIL , ICON_PHONE , ICON_PREMIUM_DAYS , ICON_LAST_ORDER , ICON_NEXT_ORDER_SINCE , \
19- ICON_NEXT_ORDER_TILL , ICON_INFO
19+ ICON_NEXT_ORDER_TILL , ICON_INFO , ICON_DELIVERY_TIME , ICON_MONTHLY_SPENT
2020from .entity import BaseEntity
2121from .hub import RohlikAccount
22+ from .utils import extract_delivery_datetime , calculate_current_month_orders_total
2223
2324SCAN_INTERVAL = timedelta (seconds = 600 )
2425
@@ -46,7 +47,9 @@ async def async_setup_entry(
4647 LastOrder (rohlik_hub ),
4748 NextOrderTill (rohlik_hub ),
4849 NextOrderSince (rohlik_hub ),
49- DeliveryInfo (rohlik_hub )
50+ DeliveryInfo (rohlik_hub ),
51+ DeliveryTime (rohlik_hub ),
52+ MonthlySpent (rohlik_hub )
5053 ]
5154
5255 if rohlik_hub .has_address :
@@ -78,135 +81,13 @@ def native_value(self) -> str | None:
7881 return None
7982
8083
81- @staticmethod
82- def extract_delivery_datetime (text : str ) -> datetime | None :
83- """
84- Extract delivery time information from various formatted strings and return a datetime object.
85-
86- Handles three types of delivery messages:
87- 1. Time only (HH:MM): "delivery at 17:23"
88- 2. Date and time: "delivery on 26.4. at 08:00"
89- 3. Minutes until delivery: "delivery in approximately 3 minutes"
90-
91- Args:
92- text: HTML text containing delivery time information
93-
94- Returns:
95- A timezone-aware datetime object representing the delivery time, or None if no valid time found
96- """
97-
98- # Replace Unicode escape sequences
99- clean_text : str = text .encode ('utf-8' ).decode ('unicode_escape' )
100-
101- # Get plain text without HTML tags for pattern detection
102- plain_text : str = re .sub (r'<[^>]+>' , '' , clean_text )
103-
104- prague_tz = ZoneInfo ('Europe/Prague' )
105- now = datetime .now (tz = prague_tz )
106- current_year : int = now .year
107-
108- # Check for Type 3: Minutes until delivery
109- if re .search (r'(přibližně za|za)\s*.*\s*(minut|minuty|min)' , plain_text , re .IGNORECASE ):
110- # Extract number of minutes from highlighted span
111- minutes_pattern : re .Pattern = re .compile (r'<span[^>]*color:[^>]*>([0-9]+)</span>' )
112-
113- matches = re .finditer (minutes_pattern , clean_text )
114- minutes_matches : list [str ] = [match .group (1 ) for match in matches ]
115-
116- if minutes_matches :
117- try :
118- minutes : int = int (minutes_matches [0 ])
119- # Calculate the estimated delivery time
120- return now + timedelta (minutes = minutes )
121- except ValueError :
122- pass
123-
124- # Check for Type 2: Date and time
125- date_pattern = re .compile (r'<span[^>]*color:[^>]*>([0-9]{1,2}\.[0-9]{1,2}\.)</span>' )
126- time_pattern = re .compile (r'<span[^>]*color:[^>]*>([0-9]{1,2}:[0-9]{2})</span>' )
127-
128- matches_date = re .finditer (date_pattern , clean_text )
129- date_matches = [match .group (1 ) for match in matches_date ]
130-
131- matches_time = re .finditer (time_pattern , clean_text )
132- time_matches = [match .group (1 ) for match in matches_time ]
133-
134- if date_matches and time_matches :
135- # We have both date and time
136- try :
137- date_str : str = date_matches [0 ] # e.g., "26.4."
138- day , month = map (int , date_str .replace ('.' , ' ' ).split ())
139-
140- time_str : str = time_matches [0 ] # e.g., "08:00"
141- hour , minute = map (int , time_str .split (':' ))
142-
143- # Create full delivery datetime
144- delivery_dt = datetime (
145- current_year , month , day , hour , minute ,
146- tzinfo = prague_tz
147- )
148-
149- return delivery_dt
150- except (ValueError , IndexError ):
151- pass
152-
153- # Check for Type 1: Time only
154- if time_matches :
155- try :
156- time_str : str = time_matches [0 ] # e.g., "17:23"
157- hour , minute = map (int , time_str .split (':' ))
158-
159- # Use today's date with the specified time
160- today = now .date ()
161-
162- # If the time has already passed today, it might refer to tomorrow
163- delivery_dt = datetime .combine (today , time (hour , minute ))
164- delivery_dt = delivery_dt .replace (tzinfo = prague_tz )
165-
166- if delivery_dt < now :
167- # Time already passed today, assume it's for tomorrow
168- tomorrow = today + timedelta (days = 1 )
169- delivery_dt = datetime .combine (tomorrow , time (hour , minute ))
170- delivery_dt = delivery_dt .replace (tzinfo = prague_tz )
171-
172- return delivery_dt
173- except (ValueError , IndexError ):
174- pass
175-
176- # If no structured time information was found, try to extract any time mention
177- # Generic time pattern search in the plain text
178- plain_time_matches = re .findall (r'\b([0-9]{1,2}:[0-9]{2})\b' , plain_text )
179- if plain_time_matches :
180- try :
181- time_str : str = plain_time_matches [0 ]
182- hour , minute = map (int , time_str .split (':' ))
183-
184- # Use today's date with the specified time
185- today = now .date ()
186-
187- delivery_dt = datetime .combine (today , time (hour , minute ))
188- delivery_dt = delivery_dt .replace (tzinfo = prague_tz )
189-
190- # If the time has already passed today, it might refer to tomorrow
191- if delivery_dt < now :
192- tomorrow = today + timedelta (days = 1 )
193- delivery_dt = datetime .combine (tomorrow , time (hour , minute ))
194- delivery_dt = delivery_dt .replace (tzinfo = prague_tz )
195-
196- return delivery_dt
197- except (ValueError , IndexError ):
198- pass
199-
200- # No valid time information found
201- return None
202-
20384 @property
20485 def extra_state_attributes (self ) -> Mapping [str , Any ] | None :
20586 """ Get extra state attributes. """
20687 delivery_info : list = self ._rohlik_account .data ["delivery_announcements" ]["data" ]["announcements" ]
20788 if len (delivery_info ) > 0 :
20889
209- delivery_time = self . extract_delivery_datetime (delivery_info [0 ].get ("content" , "" ))
90+ delivery_time = extract_delivery_datetime (delivery_info [0 ].get ("content" , "" ))
21091
21192 if delivery_info [0 ].get ("additionalContent" , None ):
21293 clean_text = delivery_info [0 ]["additionalContent" ]
@@ -215,7 +96,7 @@ def extra_state_attributes(self) -> Mapping[str, Any] | None:
21596 additional_info = None
21697
21798 return {
218- "Delivery time - experimental " : delivery_time ,
99+ "Delivery time (deprecated, use new entity) " : delivery_time ,
219100 "Order Id" : str (delivery_info [0 ].get ("id" )),
220101 "Updated At" : datetime .fromisoformat (delivery_info [0 ].get ("updatedAt" )),
221102 "Title" : delivery_info [0 ].get ("title" ),
@@ -235,6 +116,37 @@ async def async_added_to_hass(self) -> None:
235116 async def async_will_remove_from_hass (self ) -> None :
236117 self ._rohlik_account .remove_callback (self .async_write_ha_state )
237118
119+ class DeliveryTime (BaseEntity , SensorEntity ):
120+ """Sensor for showing delivery time."""
121+
122+ _attr_translation_key = "delivery_time"
123+ _attr_should_poll = False
124+ _attr_device_class = SensorDeviceClass .TIMESTAMP
125+
126+ @property
127+ def native_value (self ) -> str | None :
128+ """Returns time of delivery."""
129+ delivery_info : list = self ._rohlik_account .data ["delivery_announcements" ]["data" ]["announcements" ]
130+ if len (delivery_info ) > 0 :
131+
132+ return extract_delivery_datetime (delivery_info [0 ].get ("content" , "" ))
133+
134+ else :
135+ if self ._rohlik_account .is_ordered :
136+ return self .native_value
137+ else :
138+ return None
139+
140+
141+ @property
142+ def icon (self ) -> str :
143+ return ICON_DELIVERY_TIME
144+
145+ async def async_added_to_hass (self ) -> None :
146+ self ._rohlik_account .register_callback (self .async_write_ha_state )
147+
148+ async def async_will_remove_from_hass (self ) -> None :
149+ self ._rohlik_account .remove_callback (self .async_write_ha_state )
238150
239151class FirstExpressSlot (BaseEntity , SensorEntity ):
240152 """Sensor for first available delivery."""
@@ -512,6 +424,29 @@ async def async_will_remove_from_hass(self) -> None:
512424 self ._rohlik_account .remove_callback (self .async_write_ha_state )
513425
514426
427+ class MonthlySpent (BaseEntity , SensorEntity ):
428+ """Sensor for amount spent in current month."""
429+
430+ _attr_translation_key = "monthly_spent"
431+ _attr_should_poll = False
432+
433+ @property
434+ def native_value (self ) -> float | None :
435+ """Returns amount spend within last month."""
436+ _LOGGER .error (self ._rohlik_account .data .get ('delivered_orders' , "[]" ))
437+ return calculate_current_month_orders_total (self ._rohlik_account .data .get ('delivered_orders' , []))
438+
439+ @property
440+ def icon (self ) -> str :
441+ return ICON_MONTHLY_SPENT
442+
443+ async def async_added_to_hass (self ) -> None :
444+ self ._rohlik_account .register_callback (self .async_write_ha_state )
445+
446+ async def async_will_remove_from_hass (self ) -> None :
447+ self ._rohlik_account .remove_callback (self .async_write_ha_state )
448+
449+
515450class NoLimitOrders (BaseEntity , SensorEntity ):
516451 """Sensor for remaining no limit orders."""
517452
@@ -753,11 +688,16 @@ class UpdateSensor(BaseEntity, SensorEntity):
753688 _attr_icon = ICON_UPDATE
754689 _attr_device_class = SensorDeviceClass .TIMESTAMP
755690
756- def __init__ ( self , rohlik_account : RohlikAccount ) -> None :
757- super (). __init__ ( rohlik_account )
758- self . _attr_native_value = datetime .now (tz = ZoneInfo ("Europe/Prague" ))
691+ @ property
692+ def native_value ( self ) -> datetime :
693+ return datetime .now (tz = ZoneInfo ("Europe/Prague" ))
759694
760695 async def async_update (self ) -> None :
761696 """Calls regular update of data from API."""
762697 await self ._rohlik_account .async_update ()
763- self ._attr_native_value = datetime .now (tz = ZoneInfo ("Europe/Prague" ))
698+
699+ async def async_added_to_hass (self ) -> None :
700+ self ._rohlik_account .register_callback (self .async_write_ha_state )
701+
702+ async def async_will_remove_from_hass (self ) -> None :
703+ self ._rohlik_account .remove_callback (self .async_write_ha_state )
0 commit comments