Skip to content

Commit c2a89fa

Browse files
committed
Init
1 parent 1a6d1a5 commit c2a89fa

File tree

8 files changed

+290
-0
lines changed

8 files changed

+290
-0
lines changed

README.md

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
# Home Assistant Pyscript for Aptner
2+
3+
홈어시스턴트의 Pyscript 환경에서 대한민국 아파트 생활지원 앱 '아파트너(Aptner)'의 주요 기능을 연동하기 위한 스크립트입니다.
4+
이 스크립트를 통해 아파트너 앱에서 제공하는 관리비 내역, 차량 입/출차 기록, 방문차량 예약 등의 기능을 Home Assistant 자동화에 활용할 수 있습니다.
5+
6+
## ✨ 주요 기능
7+
8+
* **관리비 조회**: 최근 부과된 관리비 총액과 상세 내역을 가져옵니다.
9+
* **차량 입/출차 확인**: 등록된 차량의 최근 입차 또는 출차 상태와 시간을 확인합니다.
10+
* **방문차량 예약 현황**: 예약된 방문차량의 목록과 방문 기간을 조회합니다.
11+
* **방문차량 예약/관리**: 새로운 방문차량을 예약합니다.
12+
13+
## ⚙️ 사전 요구사항
14+
15+
- **Home Assistant**: 이 코드는 Home Assistant 환경에서 동작합니다.
16+
- **Pyscript**: `Pyscript` 통합 구성요소를 설치하고 설정해야 합니다. 자세한 내용은 [Pyscript](https://hacs-pyscript.readthedocs.io/en/latest/installation.html)를 참고하세요.
17+
- **아파트너 계정**: 아파트너 앱에서 사용하는 아이디와 비밀번호가 필요합니다.
18+
19+
## 📥 설치 방법
20+
21+
`aptner_pyscript.py`를 Home Assistant `<config>/pyscript` 폴더에 저장합니다. 5개의 액션이 자동으로 등록 됩니다.
22+
23+
![액션](img/services.png)
24+
<br>
25+
<br>
26+
27+
## 🚀 사용 방법
28+
29+
### 1\. 초기 로그인 (필수)
30+
31+
스크립트의 모든 기능을 사용하기 전에 반드시 아파트너 계정으로 로그인해야 합니다.
32+
33+
* **개발자 도구** \> **액션**으로 이동합니다.
34+
* 액션 검색창에서 `아파트너 로그인`을 선택합니다.
35+
* `id``password` 필드에 아파트너 계정 정보를 입력하고 **실행** 버튼을 누릅니다.
36+
* 성공적으로 호출되면 이후부터 다른 액션들을 사용할 수 있습니다.
37+
38+
![액션](img/login.png)
39+
40+
> 팁: 이 액션은 Home Assistant가 시작될 때마다 실행되는 자동화에 등록해두면 편리합니다.
41+
<br>
42+
43+
### 2\. 제공되는 액션
44+
45+
#### 가. 아파트너 관리비 (`pyscript.aptner_fee`)
46+
47+
최근 관리비 내역을 조회합니다.
48+
49+
* **파라미터**: 없음
50+
* **반환값**: 최근 관리비 세부내역
51+
52+
![관리비](img/fee.png)
53+
54+
> 팁: 이 액션은 관리비가 부과되는 날 전후로 일정시간 간격 자동으로 호출해서 결과를 attribute로 저장하는 관리비 확인용 센서를 만들면 편리합니다.
55+
<br>
56+
57+
#### 나. 아파트너 입출차확인 (`pyscript.aptner_findcar`)
58+
59+
차량의 입/출차 상태를 확인합니다.
60+
61+
* **파라미터**:
62+
* `carno` (선택 사항): 특정 차량번호를 지정합니다. 지정하지 않으면 모든 등록 차량의 마지막 상태를 반환합니다.
63+
* **반환값**: 차량의 입출차 현황
64+
65+
![입출차확인](img/carcheck.png)
66+
<br>
67+
<br>
68+
69+
#### 다. 아파트너 방문차량 예약현황 (`pyscript.aptner_get_reserve_status`)
70+
71+
현재 예약된 방문차량 목록을 조회합니다.
72+
73+
* **파라미터**: 없음
74+
* **반환값**: 연속된 예약일은 하나의 기간으로 묶어서 표시됩니다.
75+
76+
![예약현황](img/carreservechk.png)
77+
<br>
78+
<br>
79+
80+
#### 라. 아파트너 방문차량 예약 (`pyscript.aptner_reserve_car`)
81+
82+
방문차량 주차를 예약합니다.
83+
84+
* **파라미터**:
85+
* `date` (필수): 방문 시작일 (예: `2025.01.01`)
86+
* `purpose` (필수): 방문 목적 (예: `기타`)
87+
* `carno` (필수): 차량번호 (예: `111가1111`)
88+
* `days` (필수): 방문 기간(일) (예: `1`)
89+
* `phone` (필수): 운전자 연락처 (예: `010-0000-0000`)
90+
91+
![예약](img/carreserve.png)

aptner_pyscript.py

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
import aiohttp
2+
import asyncio
3+
from datetime import datetime, timedelta
4+
5+
APTNER_LOGIN = {}
6+
APTNER_BASEURL = 'https://v2.aptner.com'
7+
APTNER_HEADERS = { 'Content-Type': 'application/json' }
8+
APTNER_AUTH_RUNNING = False
9+
APTNER_AUTH_COND = asyncio.Condition()
10+
APTNER_SESSION = None
11+
12+
@pyscript_compile
13+
def aptner_session(**kwargs):
14+
global APTNER_SESSION
15+
if APTNER_SESSION is None or APTNER_SESSION.closed:
16+
APTNER_SESSION = aiohttp.ClientSession(**kwargs)
17+
return APTNER_SESSION
18+
19+
@time_trigger('shutdown')
20+
def aptner_close_session():
21+
global APTNER_SESSION
22+
if APTNER_SESSION and not APTNER_SESSION.closed:
23+
try:
24+
APTNER_SESSION.close()
25+
finally:
26+
APTNER_SESSION = None
27+
28+
@pyscript_compile
29+
async def aptner_auth():
30+
global APTNER_LOGIN, APTNER_HEADERS, APTNER_AUTH_RUNNING, APTNER_AUTH_COND
31+
async with APTNER_AUTH_COND:
32+
if APTNER_AUTH_RUNNING:
33+
try:
34+
await asyncio.wait_for(APTNER_AUTH_COND.wait(), timeout = 30)
35+
finally:
36+
return
37+
APTNER_AUTH_RUNNING = True
38+
try:
39+
if 'Authorization' in APTNER_HEADERS:
40+
del APTNER_HEADERS['Authorization']
41+
response = await aptner_request('POST', '/auth/token', data = APTNER_LOGIN)
42+
APTNER_HEADERS['Authorization'] = 'Bearer ' + response['accessToken']
43+
finally:
44+
async with APTNER_AUTH_COND:
45+
APTNER_AUTH_RUNNING = False
46+
APTNER_AUTH_COND.notify_all()
47+
48+
@pyscript_compile
49+
async def aptner_request(method, url, data = None):
50+
global APTNER_HEADERS, APTNER_BASEURL
51+
kwargs = { 'headers': APTNER_HEADERS }
52+
if method in ('PUT', 'POST') and data is not None:
53+
kwargs['json'] = data
54+
session = aptner_session(base_url = APTNER_BASEURL)
55+
async with session.request(method, url, **kwargs) as response:
56+
if response.status != 401 or url == '/auth/token':
57+
response.raise_for_status()
58+
try:
59+
return await response.json()
60+
except:
61+
return
62+
await aptner_auth()
63+
async with session.request(method, url, **kwargs) as response:
64+
response.raise_for_status()
65+
try:
66+
return await response.json()
67+
except:
68+
return
69+
70+
@service(supports_response = 'only')
71+
def aptner_init(id, password):
72+
"""yaml
73+
name: 아파트너 로그인
74+
description: 최초 아파트너 로그인 필요
75+
fields:
76+
id:
77+
description: 아파트너아이디
78+
example: aptner
79+
required: true
80+
password:
81+
description: 아파트너암호
82+
example: password
83+
required: true
84+
"""
85+
global APTNER_LOGIN
86+
APTNER_LOGIN['id'] = id
87+
APTNER_LOGIN['password'] = password
88+
try:
89+
aptner_auth()
90+
if 'Authorization' in APTNER_HEADERS:
91+
return { 'result': 'success' }
92+
except Exception as e:
93+
return { 'error': '{}: {}'.format(type(e).__name__, str(e)) }
94+
return { 'result': 'unknown' }
95+
96+
@service(supports_response = 'only')
97+
def aptner_findcar(carno = None):
98+
"""yaml
99+
name: 아파트너 입출차확인
100+
description: 아파트너에서 차량의 입/출차를 확인합니다
101+
fields:
102+
carno:
103+
description: 차량번호
104+
example: 123가1234
105+
"""
106+
monthlyAccessHistory = aptner_request('GET', '/pc/monthly-access-history')
107+
response = {}
108+
for monthlyParkingHistory in monthlyAccessHistory['monthlyParkingHistoryList']:
109+
for visitCarUseHistoryReport in monthlyParkingHistory['visitCarUseHistoryReportList']:
110+
if carno == None or visitCarUseHistoryReport['carNo'] == carno:
111+
if visitCarUseHistoryReport['carNo'] not in response:
112+
response[visitCarUseHistoryReport['carNo']] = {
113+
'status': 'out' if visitCarUseHistoryReport['isExit'] else 'in',
114+
}
115+
if 'inDatetime' in visitCarUseHistoryReport and visitCarUseHistoryReport['inDatetime'] is not None:
116+
response[visitCarUseHistoryReport['carNo']]['intime'] = visitCarUseHistoryReport['inDatetime']
117+
if 'outDatetime' in visitCarUseHistoryReport and visitCarUseHistoryReport['outDatetime'] is not None:
118+
response[visitCarUseHistoryReport['carNo']]['outtime'] = visitCarUseHistoryReport['outDatetime']
119+
if visitCarUseHistoryReport['carNo'] == carno:
120+
break
121+
return response
122+
123+
@service(supports_response = 'only')
124+
def aptner_fee():
125+
"""yaml
126+
name: 아파트너 관리비
127+
description: 아파트너에서 최근 관리비를 확인합니다
128+
"""
129+
fee = aptner_request('GET', '/fee/detail')['fee']
130+
return { 'year': fee['year'], 'month': fee['month'], 'fee': fee['currentFee'], 'details': { item["name"]: item["value"] for item in fee['details'] }}
131+
132+
@service(supports_response = 'only')
133+
def aptner_get_reserve_status():
134+
"""yaml
135+
name: 아파트너 방문차량 예약현황
136+
description: 아파트너에서 방문차량의 주차 예약현황을 확인합니다
137+
"""
138+
totalpages = 0
139+
currentpage = 0
140+
today = datetime.today().date()
141+
result = {}
142+
while True:
143+
currentpage = currentpage + 1
144+
reservedcars = aptner_request('GET', '/pc/reserves?pg={}'.format(currentpage))
145+
if totalpages == 0:
146+
totalpages = reservedcars['totalPages']
147+
for reservedcar in reservedcars['reserveList']:
148+
visitdate = datetime.strptime(reservedcar['visitDate'], "%Y.%m.%d").date()
149+
if today < visitdate:
150+
if reservedcar['carNo'] not in result:
151+
result[reservedcar['carNo']] = []
152+
result[reservedcar['carNo']].append(visitdate)
153+
if currentpage >= totalpages:
154+
break
155+
for car in result:
156+
result[car].sort()
157+
ranges = []
158+
start_of_range = result[car][0]
159+
for i in range(1, len(result[car])):
160+
previous_date = result[car][i-1]
161+
current_date = result[car][i]
162+
if (current_date - previous_date) > timedelta(days = 1):
163+
ranges.append({ 'from': start_of_range, 'to': previous_date })
164+
start_of_range = current_date
165+
ranges.append({ 'from': start_of_range, 'to': result[car][-1] })
166+
result[car] = ranges
167+
return result
168+
169+
@service()
170+
def aptner_reserve_car(date, purpose, carno, days, phone):
171+
"""yaml
172+
name: 아파트너 방문차량 예약
173+
description: 아파트너에서 방문차량의 주차를 예약합니다
174+
fields:
175+
date:
176+
description: 방문일시
177+
example: 2025.01.01
178+
required: true
179+
purpose:
180+
description: 방문목적
181+
example: 기타
182+
required: true
183+
carno:
184+
description: 차량번호
185+
example: 111가1111
186+
required: true
187+
days:
188+
description: 방문기간
189+
example: 1
190+
required: true
191+
phone:
192+
description: 연락처
193+
example: 010-0000-0000
194+
required: true
195+
"""
196+
try:
197+
aptner_request('POST', '/pc/reserve/', { 'visitDate': date, 'purpose': purpose, 'carNo': carno, 'days': days, 'phone': phone })
198+
except:
199+
pass
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.

0 commit comments

Comments
 (0)