Skip to content

Commit ee447a5

Browse files
committed
feat: Major update for flatlib_astrology integration
- Replaced text inputs with Selectors for user and timezone. - Added new DataUpdateCoordinators for daily, monthly, and yearly horoscopes. - Implemented API calls for new prediction endpoints. - Updated API client and config flow to support new features.
1 parent 344d9b6 commit ee447a5

8 files changed

Lines changed: 313 additions & 100 deletions

File tree

flatlib_astrology/__init__.py

Lines changed: 109 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,67 +1,140 @@
1-
"""The Flatlib Natal Chart integration."""
1+
# __init__.py
22
import logging
3-
from datetime import timedelta
3+
from datetime import timedelta, datetime
4+
import pytz
5+
46
from homeassistant.config_entries import ConfigEntry
57
from homeassistant.core import HomeAssistant
68
from homeassistant.helpers.aiohttp_client import async_get_clientsession
79
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
8-
from .const import DOMAIN
10+
11+
from .const import DOMAIN, PLATFORMS
912
from .api import ApiClient, ApiError
1013

1114
_LOGGER = logging.getLogger(__name__)
12-
PLATFORMS = ["sensor"]
15+
16+
DAILY_INTERVAL = timedelta(hours=24)
17+
MONTHLY_INTERVAL = timedelta(hours=24)
18+
YEARLY_INTERVAL = timedelta(hours=24)
19+
20+
def _convert_timezone_to_offset(tz_name: str) -> str:
21+
"""Преобразует имя часового пояса в строковое смещение UTC в формате +HH:MM."""
22+
try:
23+
tz_object = pytz.timezone(tz_name)
24+
offset_seconds = tz_object.utcoffset(datetime.now()).total_seconds()
25+
26+
offset_hours = int(offset_seconds / 3600)
27+
offset_minutes = int((offset_seconds % 3600) / 60)
28+
29+
sign = "+" if offset_seconds >= 0 else "-"
30+
31+
return f"{sign}{abs(offset_hours):02d}:{abs(offset_minutes):02d}"
32+
except (pytz.UnknownTimeZoneError, AttributeError):
33+
return "+00:00"
34+
35+
def _get_api_payload(entry: ConfigEntry) -> dict:
36+
"""Создает полезную нагрузку для запроса API из данных конфигурации."""
37+
converted_tz = _convert_timezone_to_offset(entry.data.get("time_zone"))
38+
39+
# Теперь отправляем дату в стандартном формате YYYY-MM-DD,
40+
# который Home Assistant возвращает и который, как показывает ошибка, ожидает сервер.
41+
birth_date_str = entry.data.get("birth_date")
42+
43+
return {
44+
"date": birth_date_str, # Отправляем дату как есть (YYYY-MM-DD)
45+
"time": entry.data.get("birth_time"),
46+
"tz": converted_tz,
47+
"lat": entry.data.get("location", {}).get("latitude"),
48+
"lon": entry.data.get("location", {}).get("longitude"),
49+
}
1350

1451
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
15-
"""Set up Flatlib Natal Chart from a config entry."""
16-
# Используем общую сессию aiohttp, управляемую Home Assistant
52+
"""Настройка интеграции Flatlib из записи конфигурации."""
1753
session = async_get_clientsession(hass)
1854
api_client = ApiClient(session)
19-
20-
# --- ИСПРАВЛЕНИЕ: Получаем данные напрямую, без сложной обработки ---
21-
entry_data = entry.data
22-
_LOGGER.debug("Setting up Flatlib Natal for: %s", entry_data["name"])
2355

24-
async def async_update_data():
25-
"""Fetch data from the flatlib-server API."""
56+
async def async_update_natal_data():
57+
"""Получить данные натальной карты."""
58+
payload = _get_api_payload(entry)
2659
try:
27-
location_data = entry.data["location"]
28-
# --- ИСПРАВЛЕНИЕ: Корректно форматируем дату и время ---
29-
payload = {
30-
"date": entry_data["birth_date"],
31-
"time": entry_data["birth_time"],
32-
"tz": entry_data["time_zone"],
33-
"lat": str(location_data["latitude"]),
34-
"lon": str(location_data["longitude"]),
35-
}
36-
_LOGGER.debug("Sending payload to API: %s", payload)
3760
return await api_client.get_natal_chart(payload)
3861
except ApiError as err:
39-
raise UpdateFailed(f"Error communicating with API: {err}")
40-
except KeyError as err:
41-
raise UpdateFailed(f"Missing data in config entry: {err}")
62+
raise UpdateFailed(f"Ошибка получения натальной карты: {err}")
4263

43-
coordinator = DataUpdateCoordinator(
64+
natal_coordinator = DataUpdateCoordinator(
4465
hass,
4566
_LOGGER,
46-
name=f"flatlib_natal_{entry_data['name']}",
47-
update_method=async_update_data,
48-
update_interval=timedelta(hours=24), # Обновляем раз в сутки
67+
name=f"flatlib_natal_{entry.data['name']}",
68+
update_method=async_update_natal_data,
69+
update_interval=DAILY_INTERVAL,
4970
)
5071

51-
# Выполняем первое обновление при запуске
52-
await coordinator.async_config_entry_first_refresh()
72+
async def async_update_daily_data():
73+
"""Получить данные ежедневного предсказания."""
74+
payload = _get_api_payload(entry)
75+
try:
76+
return await api_client.async_get_daily_prediction(payload)
77+
except ApiError as err:
78+
raise UpdateFailed(f"Ошибка получения ежедневного предсказания: {err}")
5379

54-
hass.data.setdefault(DOMAIN, {})
55-
hass.data[DOMAIN][entry.entry_id] = coordinator
80+
daily_prediction_coordinator = DataUpdateCoordinator(
81+
hass,
82+
_LOGGER,
83+
name=f"flatlib_daily_prediction_{entry.data['name']}",
84+
update_method=async_update_daily_data,
85+
update_interval=DAILY_INTERVAL,
86+
)
87+
88+
async def async_update_monthly_data():
89+
"""Получить данные ежемесячного предсказания."""
90+
payload = _get_api_payload(entry)
91+
try:
92+
return await api_client.async_get_monthly_prediction(payload)
93+
except ApiError as err:
94+
raise UpdateFailed(f"Ошибка получения ежемесячного предсказания: {err}")
5695

57-
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
96+
monthly_prediction_coordinator = DataUpdateCoordinator(
97+
hass,
98+
_LOGGER,
99+
name=f"flatlib_monthly_prediction_{entry.data['name']}",
100+
update_method=async_update_monthly_data,
101+
update_interval=MONTHLY_INTERVAL,
102+
)
58103

104+
async def async_update_yearly_data():
105+
"""Получить данные ежегодного предсказания."""
106+
payload = _get_api_payload(entry)
107+
try:
108+
return await api_client.async_get_yearly_prediction(payload)
109+
except ApiError as err:
110+
raise UpdateFailed(f"Ошибка получения ежегодного предсказания: {err}")
111+
112+
yearly_prediction_coordinator = DataUpdateCoordinator(
113+
hass,
114+
_LOGGER,
115+
name=f"flatlib_yearly_prediction_{entry.data['name']}",
116+
update_method=async_update_yearly_data,
117+
update_interval=YEARLY_INTERVAL,
118+
)
119+
120+
await natal_coordinator.async_config_entry_first_refresh()
121+
await daily_prediction_coordinator.async_config_entry_first_refresh()
122+
await monthly_prediction_coordinator.async_config_entry_first_refresh()
123+
await yearly_prediction_coordinator.async_config_entry_first_refresh()
124+
125+
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {
126+
"natal_coordinator": natal_coordinator,
127+
"daily_prediction_coordinator": daily_prediction_coordinator,
128+
"monthly_prediction_coordinator": monthly_prediction_coordinator,
129+
"yearly_prediction_coordinator": yearly_prediction_coordinator,
130+
}
131+
132+
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
59133
return True
60134

61135
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
62-
"""Unload a config entry."""
136+
"""Выгрузить запись конфигурации."""
63137
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
64138
if unload_ok:
65139
hass.data[DOMAIN].pop(entry.entry_id)
66-
67140
return unload_ok

flatlib_astrology/api.py

Lines changed: 46 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,64 @@
1-
"""API Client for the Flatlib Natal Chart Add-on."""
1+
# api.py
22
import aiohttp
33
import asyncio
44
from typing import Dict, Any
5+
from datetime import datetime
6+
from dateutil.relativedelta import relativedelta # Новый импорт
57

68
class ApiClient:
7-
"""API Client to communicate with the Flatlib server."""
8-
99
def __init__(self, session: aiohttp.ClientSession):
10-
"""Initialize the API client."""
1110
self._session = session
12-
# Supervisor DNS резолвит slug аддона в его IP-адрес
13-
self._api_url = f"http://homeassistant.local:8080/natal"
11+
self._base_url = "http://localhost:8080"
1412

1513
async def get_natal_chart(self, payload: Dict[str, Any]) -> Dict[str, Any]:
16-
"""Get natal chart data from the add-on."""
14+
url = f"{self._base_url}/natal"
1715
try:
18-
async with self._session.post(self._api_url, json=payload) as response:
16+
async with self._session.post(url, json=payload) as response:
1917
response.raise_for_status()
2018
return await response.json()
2119
except asyncio.TimeoutError:
2220
raise ApiError("Timeout communicating with API")
2321
except aiohttp.ClientError as err:
2422
raise ApiError(f"Error communicating with API: {err}")
2523

24+
async def async_get_daily_prediction(self, payload: Dict[str, Any]) -> Dict[str, Any]:
25+
url = f"{self._base_url}/predict/daily"
26+
27+
today_str = datetime.now().strftime('%Y/%m/%d')
28+
prediction_payload = {**payload, "target_date": today_str}
29+
30+
try:
31+
async with self._session.post(url, json=prediction_payload) as response:
32+
response.raise_for_status()
33+
return await response.json()
34+
except asyncio.TimeoutError:
35+
raise ApiError("Timeout communicating with API for daily prediction")
36+
except aiohttp.ClientError as err:
37+
raise ApiError(f"Error communicating with API for daily prediction: {err}")
38+
39+
# --- НОВЫЙ МЕТОД: Месячный прогноз ---
40+
async def async_get_monthly_prediction(self, payload: Dict[str, Any]) -> Dict[str, Any]:
41+
url = f"{self._base_url}/predict/monthly"
42+
try:
43+
async with self._session.post(url, json=payload) as response:
44+
response.raise_for_status()
45+
return await response.json()
46+
except asyncio.TimeoutError:
47+
raise ApiError("Timeout communicating with API for monthly prediction")
48+
except aiohttp.ClientError as err:
49+
raise ApiError(f"Error communicating with API for monthly prediction: {err}")
50+
51+
# --- НОВЫЙ МЕТОД: Годовой прогноз ---
52+
async def async_get_yearly_prediction(self, payload: Dict[str, Any]) -> Dict[str, Any]:
53+
url = f"{self._base_url}/predict/yearly"
54+
try:
55+
async with self._session.post(url, json=payload) as response:
56+
response.raise_for_status()
57+
return await response.json()
58+
except asyncio.TimeoutError:
59+
raise ApiError("Timeout communicating with API for yearly prediction")
60+
except aiohttp.ClientError as err:
61+
raise ApiError(f"Error communicating with API for yearly prediction: {err}")
62+
2663
class ApiError(Exception):
27-
"""Exception to indicate a general API error."""
64+
"""Исключение для обозначения общей ошибки API."""

flatlib_astrology/config_flow.py

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,36 +6,72 @@
66
DateSelector,
77
TimeSelector,
88
LocationSelector,
9+
SelectSelector,
10+
SelectSelectorConfig,
911
)
1012
from .const import DOMAIN
13+
import pytz # <-- Новый импорт!
1114

1215
class FlatlibNatalConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
1316
VERSION = 1
1417

1518
async def async_step_user(self, user_input=None):
16-
"""Handle the initial step."""
19+
"""Обработка начального шага."""
1720
errors = {}
1821

22+
all_users = await self.hass.auth.async_get_users()
23+
excluded_names = ["Supervisor", "Home Assistant Content"]
24+
filtered_users = [user for user in all_users if user.name not in excluded_names]
25+
user_options = {user.id: user.name for user in filtered_users}
26+
27+
# Получаем полный список часовых поясов
28+
timezones = pytz.all_timezones
29+
timezone_options = [{"value": tz, "label": tz} for tz in timezones]
30+
1931
if user_input is not None:
20-
unique_id = user_input["name"].strip().lower().replace(" ", "_")
21-
await self.async_set_unique_id(unique_id)
32+
user_id = user_input["user_id"]
33+
user_name = user_options.get(user_id, "Unknown User")
34+
35+
await self.async_set_unique_id(user_id)
2236
self._abort_if_unique_id_configured()
37+
38+
data = {
39+
"user_id": user_id,
40+
"name": user_name,
41+
"birth_date": user_input["birth_date"],
42+
"birth_time": user_input["birth_time"],
43+
"location": user_input["location"],
44+
"time_zone": user_input["time_zone"],
45+
}
46+
2347
return self.async_create_entry(
24-
title=user_input["name"],
25-
data=user_input
48+
title=user_name,
49+
data=data
2650
)
2751

28-
# Правильное создание схемы с использованием vol.Schema
2952
data_schema={
30-
"name": TextSelector(TextSelectorConfig(type="text")),
53+
"user_id": SelectSelector(
54+
SelectSelectorConfig(
55+
options=[
56+
{"value": user_id, "label": user_name}
57+
for user_id, user_name in user_options.items()
58+
],
59+
mode="dropdown",
60+
)
61+
),
3162
"birth_date": DateSelector(),
3263
"birth_time": TimeSelector(),
3364
"location": LocationSelector(),
34-
"time_zone": TextSelector(TextSelectorConfig(type="text")),
65+
"time_zone": SelectSelector( # <-- Теперь это SelectSelector!
66+
SelectSelectorConfig(
67+
options=timezone_options,
68+
mode="dropdown",
69+
)
70+
),
3571
}
3672

3773
return self.async_show_form(
38-
step_id="user",
74+
step_id="user",
3975
data_schema=vol.Schema(data_schema),
4076
errors=errors
4177
)

flatlib_astrology/const.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
DOMAIN = "flatlib_astrology"
44

5+
PLATFORMS = ["sensor"]
6+
57
# Planet display names
68
PLANETS = {
79
"Sun": "Sun",

flatlib_astrology/manifest.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@
22
"domain": "flatlib_astrology",
33
"name": "Flatlib Astrology",
44
"config_flow": true,
5+
"application_credentials": false,
56
"documentation": "https://github.com/navi-vonamut/hass-flatlib-integration/blob/main/README.md",
67
"iot_class": "local_polling",
78
"issue_tracker": "https://github.com/navi-vonamut/hass-flatlib-integration/issues",
8-
"version": "0.1.0",
9+
"version": "0.2.0",
910
"codeowners": ["@navi-vonamut"],
1011
"requirements": ["aiohttp"]
1112
}

0 commit comments

Comments
 (0)