629 lines
22 KiB
Python
629 lines
22 KiB
Python
# -*- coding: utf-8 -*-
|
||
from __future__ import print_function
|
||
import requests
|
||
from datetime import datetime, tzinfo
|
||
from copy import deepcopy
|
||
|
||
|
||
class AuthException(Exception):
|
||
pass
|
||
|
||
class EmpServerException(Exception):
|
||
pass
|
||
|
||
|
||
class Client(object):
|
||
"""
|
||
Клиента
|
||
"""
|
||
def __init__(self, **kwargs):
|
||
self.token = kwargs.get('token')
|
||
self.guid = kwargs.get('guid')
|
||
|
||
#requests
|
||
self.verify = kwargs.get('verify', True)
|
||
self.timeout = kwargs.get('timeout', 3.0)
|
||
|
||
self.session_id = None # emp mos ru
|
||
self.user_agent = kwargs.get('user_agent')
|
||
self.dev_app_version = kwargs.get('dev_app_version')
|
||
self.dev_user_agent = kwargs.get('dev_user_agent')
|
||
self.headers = {'Cache-Control': 'no-cache',
|
||
'Host': 'emp.mos.ru',
|
||
'Connection': 'Keep-Alive',
|
||
'Accept-Encoding': 'gzip',
|
||
'User-Agent': self.user_agent}
|
||
self.pheaders = deepcopy(self.headers)
|
||
self.pheaders.update({
|
||
'X-Cache-ov': '15552000',
|
||
'X-Cache-ov-mode': 'FORCE_NETWORK'
|
||
})
|
||
self.session = requests.Session()
|
||
|
||
def raise_for_status(self, answer):
|
||
"""
|
||
:param answer: JSON
|
||
{
|
||
u'errorCode': 0, # 0-OK,
|
||
401-Auth error
|
||
u'execTime': 0.138262,
|
||
u'errorMessage': u'', # error message
|
||
u'session_id': u'6c3333333c33333e44e21e7d43c46e03',
|
||
u'result': None or JSON
|
||
}
|
||
:return:
|
||
"""
|
||
# print('Exec time:', answer['execTime'])
|
||
if answer['errorCode'] == 401:
|
||
raise AuthException()
|
||
|
||
if answer['errorCode'] != 0:
|
||
if answer['errorMessage']:
|
||
raise EmpServerException(u'{0} (code:{1})'.format(answer['errorMessage'], answer['errorCode']))
|
||
raise EmpServerException(u'code:{0}'.format(answer['errorCode']))
|
||
|
||
def is_active(self):
|
||
"""
|
||
:return: True если уже залогинился
|
||
"""
|
||
return self.session_id is not None
|
||
|
||
def login(self, telephone, pwd):
|
||
"""
|
||
:param telephone: Телефон. Вид: 7xxxxxxxxxx
|
||
:param pwd: Пароль из приложения Госуслуги Москвы
|
||
:return: JSON
|
||
{
|
||
u'is_filled': True,
|
||
u'surname': u'x',
|
||
u'name': u'x',
|
||
u'session_id': u'6c3333333c33333e44e21e7d43c46e03'},
|
||
u'request_id': u'UN=PRO-12345678-1234-1234-1234-123456789123'
|
||
}
|
||
"""
|
||
login_data = {
|
||
'device_info': {
|
||
'guid': self.guid,
|
||
'user_agent': self.dev_user_agent,
|
||
'app_version': self.dev_app_version
|
||
},
|
||
'auth': {
|
||
'login': telephone,
|
||
'password': pwd,
|
||
'guid': self.guid
|
||
}
|
||
}
|
||
|
||
ret = self.session.post('https://emp.mos.ru/v1.0/auth/virtualLogin',
|
||
params={'token': self.token},
|
||
headers={'Content-Type': 'application/json; charset=UTF-8',
|
||
'Connection': 'Keep-Alive',
|
||
'Accept-Encoding': 'gzip',
|
||
'User-Agent': self.user_agent,
|
||
'cache-control': 'no-cache',
|
||
'Host': 'emp.mos.ru',
|
||
'Accept': '*/*'},
|
||
verify=self.verify,
|
||
timeout=self.timeout,
|
||
json=login_data)
|
||
|
||
response = ret.json()
|
||
self.raise_for_status(response)
|
||
self.session_id = response['session_id']
|
||
return response['result']
|
||
|
||
def get_profile(self):
|
||
"""
|
||
:return: JSON
|
||
{
|
||
u'drive_license': None,
|
||
u'firstname': u'x',
|
||
u'middlename': u'x',
|
||
u'lastname': u'x',
|
||
u'birthdate': u'dd.mm.YYYY',
|
||
u'msisdn': u'71234567890', # your telephone
|
||
u'email_confirmed': True,
|
||
u'email': u'x@x.ru'
|
||
}
|
||
"""
|
||
assert self.session_id
|
||
|
||
ret = self.session.get('https://emp.mos.ru/v1.0/profile/get',
|
||
params={'token': self.token,
|
||
'info[guid]': self.guid,
|
||
'auth[session_id]': self.session_id
|
||
},
|
||
headers=self.pheaders,
|
||
verify=self.verify,
|
||
timeout=self.timeout)
|
||
|
||
response = ret.json()
|
||
self.raise_for_status(response)
|
||
return response['result']['profile']
|
||
|
||
def get_flats(self):
|
||
"""
|
||
:return: JSON array
|
||
[{
|
||
u'name': u'x',
|
||
u'paycode': u'1234567890',
|
||
u'flat_id': u'1234567',
|
||
u'electro_account': None,
|
||
u'flat_number': u'0',
|
||
u'unad': u'0',
|
||
u'address': u'x',
|
||
u'electro_device': None,
|
||
u'unom': u'1234567' # Идентификатор дома
|
||
},{...}
|
||
]
|
||
"""
|
||
assert self.session_id
|
||
|
||
ret = self.session.get('https://emp.mos.ru/v1.0/flat/get',
|
||
params={'token': self.token,
|
||
'info[guid]': self.guid,
|
||
'auth[session_id]': self.session_id
|
||
},
|
||
headers=self.pheaders,
|
||
verify=self.verify,
|
||
timeout = self.timeout)
|
||
|
||
response = ret.json()
|
||
self.raise_for_status(response)
|
||
return response['result']
|
||
|
||
def get_watercounters(self, flat_id):
|
||
"""
|
||
:param flat_id: unicode string from flat_response
|
||
response[0]['flat_id']
|
||
:return:
|
||
{
|
||
u'stat_title': u'x',
|
||
u'archive': [
|
||
{
|
||
u'cold_indication': 1.55,
|
||
u'hot_indication': 1.2,
|
||
u'period': u'2017-08-31+03:00'
|
||
}, {
|
||
u'cold_indication': 1.3,
|
||
u'hot_indication': 0.39999999999998,
|
||
u'period': u'2017-09-30+03:00'
|
||
}, {
|
||
+10 эл-тов. Всего 12.
|
||
}],
|
||
u'counters': [
|
||
{
|
||
u'checkup': u'2021-08-08+03:00', # дата поверки
|
||
u'counterId': 123456, # id счетчика в emp.mos.ru
|
||
u'num': u'123456', # серийный номер счетчика
|
||
u'type': 1, # ХВС
|
||
u'indications': [
|
||
{
|
||
u'indication': u'200.24',
|
||
u'period': u'2018-07-31+03:00'
|
||
}, {
|
||
u'indication': u'200.6',
|
||
u'period': u'2018-06-30+03:00'
|
||
}, {
|
||
u'indication': u'200.5',
|
||
u'period': u'2018-05-31+03:00'
|
||
}, {
|
||
u'indication': u'200.5',
|
||
u'period': u'2018-04-30+03:00'
|
||
}]
|
||
},
|
||
{..}
|
||
]
|
||
}
|
||
"""
|
||
assert self.session_id
|
||
wheaders = deepcopy(self.headers)
|
||
wheaders.update({
|
||
'X-Clears-tags': 'WATER_COUNTERS',
|
||
'X-Cache-ov-mode': 'DEFAULT',
|
||
'Content-Type': 'application/json; charset=UTF-8' })
|
||
|
||
wcrequest = {
|
||
'flat_id': flat_id,
|
||
'is_widget': False,
|
||
'info': {
|
||
'guid': self.guid
|
||
},
|
||
'auth': {
|
||
'session_id': self.session_id
|
||
}
|
||
}
|
||
|
||
ret = self.session.post('https://emp.mos.ru/v1.0/watercounters/get',
|
||
params={'token': self.token},
|
||
headers=wheaders,
|
||
verify=self.verify,
|
||
timeout=self.timeout,
|
||
json=wcrequest)
|
||
|
||
response = ret.json()
|
||
self.raise_for_status(response)
|
||
return response['result']
|
||
|
||
def send_watercounters(self, flat_id, counters_data):
|
||
"""
|
||
:param flat_id: flat_response['flat_id']
|
||
:param counters_data: array of
|
||
[{
|
||
'counter_id': u'123456', # ['counters'][0]['counterId']
|
||
'period': datetime.now().strftime("%Y-%m-%d"),
|
||
'indication': 'xxx,xx'
|
||
}, {
|
||
...
|
||
}]
|
||
:return:
|
||
"""
|
||
assert self.session_id
|
||
|
||
wheaders = deepcopy(self.headers)
|
||
wheaders.update({
|
||
'X-Clears-tags': 'WATER_COUNTERS',
|
||
'Content-Type': 'application/json; charset=UTF-8'
|
||
})
|
||
|
||
wcrequest = {'flat_id': flat_id,
|
||
'counters_data': counters_data,
|
||
'info': {
|
||
'guid': self.guid
|
||
},
|
||
'auth': {
|
||
'session_id': self.session_id
|
||
}}
|
||
|
||
ret = self.session.post('https://emp.mos.ru/v1.0/watercounters/addValues',
|
||
params={'token': self.token},
|
||
headers=wheaders,
|
||
verify=self.verify,
|
||
timeout=self.timeout,
|
||
json=wcrequest)
|
||
|
||
response = ret.json()
|
||
self.raise_for_status(response)
|
||
return response['result']
|
||
|
||
def get_electrocounters(self, flat_id):
|
||
"""
|
||
:param flat_id: unicode string from flat_response
|
||
response[0]['flat_id']
|
||
:return:
|
||
{
|
||
"address": "город ...",
|
||
"electro_account": "851xxxx475",
|
||
"electro_device": "04xxx17",
|
||
"balance": 0,
|
||
"is_debt": false,
|
||
"description": "Внимание! Передача показаний возможна с 15 по 26 число месяца. При вводе текущих показаний обращайте внимание на значность счетчиков. В случае если показания введены с неверной значностью (большей, чем значность счетчика), они не будут загружены.",
|
||
"sh_znk": 5,
|
||
"zones": [{
|
||
"name": "T1",
|
||
"value": "6887"
|
||
}],
|
||
"intervals": [{
|
||
"name": "T1",
|
||
"value": "00:00-23:59"
|
||
}]
|
||
}
|
||
"""
|
||
assert self.session_id
|
||
wheaders = deepcopy(self.headers)
|
||
wheaders.update({
|
||
'X-Clears-tags': 'ELECTRO_COUNTERS',
|
||
'X-Cache-ov-mode': 'DEFAULT',
|
||
'Content-Type': 'application/json; charset=UTF-8' })
|
||
|
||
wcrequest = {
|
||
'flat_id': flat_id,
|
||
'is_widget': False,
|
||
'info': {
|
||
'guid': self.guid,
|
||
'user_agent': self.dev_user_agent,
|
||
'app_version': self.dev_app_version
|
||
},
|
||
'auth': {
|
||
'session_id': self.session_id
|
||
}
|
||
}
|
||
|
||
ret = self.session.post('https://emp.mos.ru/v1.0/electrocounters/get',
|
||
params={'token': self.token},
|
||
headers=wheaders,
|
||
verify=self.verify,
|
||
timeout=self.timeout,
|
||
json=wcrequest)
|
||
|
||
response = ret.json()
|
||
self.raise_for_status(response)
|
||
return response['result']
|
||
|
||
def send_electrocounters(self, flat_id, counters_data):
|
||
"""
|
||
:param flat_id: flat_response['flat_id']
|
||
:param counters_data: array of
|
||
[{
|
||
'counter_id': u'123456', # ['counters'][0]['counterId']
|
||
'period': datetime.now().strftime("%Y-%m-%d"),
|
||
'indication': 'xxx,xx'
|
||
}, {
|
||
...
|
||
}]
|
||
:return:
|
||
"""
|
||
assert self.session_id
|
||
|
||
wheaders = deepcopy(self.headers)
|
||
wheaders.update({
|
||
'X-Clears-tags': 'ELECTRO_COUNTERS',
|
||
'Content-Type': 'application/json; charset=UTF-8'
|
||
})
|
||
|
||
wcrequest = {'flat_id': flat_id,
|
||
'counters_data': counters_data,
|
||
'info': {
|
||
'guid': self.guid,
|
||
'user_agent': self.dev_user_agent,
|
||
'app_version': self.dev_app_version
|
||
},
|
||
'auth': {
|
||
'session_id': self.session_id
|
||
}}
|
||
ret = self.session.post('https://emp.mos.ru/v1.0/electrocounters/addValues',
|
||
params={'token': self.token},
|
||
headers=wheaders,
|
||
verify=self.verify,
|
||
timeout=self.timeout,
|
||
json=wcrequest)
|
||
|
||
response = ret.json()
|
||
self.raise_for_status(response)
|
||
return response['result']
|
||
|
||
def get_epd(self, flat_id, period, is_debt=True):
|
||
"""
|
||
:param flat_id: unicode string from flat_response
|
||
response[0]['flat_id']
|
||
:param period: unicode string represents date in 27.09.2018 format
|
||
:param is_debt: True/False
|
||
:return:
|
||
{
|
||
"is_debt": true,
|
||
"is_paid": true,
|
||
"amount": 2982.77,
|
||
"service_code": "emp.zkh",
|
||
"insurance": 61.93,
|
||
"ammount_insurance": 3044.7
|
||
}
|
||
"""
|
||
assert self.session_id
|
||
wheaders = deepcopy(self.headers)
|
||
wheaders.update({
|
||
'X-Clears-tags': 'EPD',
|
||
'X-Cache-ov-mode': 'DEFAULT',
|
||
'Content-Type': 'application/json; charset=UTF-8' })
|
||
|
||
wcrequest = {
|
||
'flat_id': flat_id,
|
||
'period': period,
|
||
'is_debt': is_debt,
|
||
'info': {
|
||
'guid': self.guid
|
||
},
|
||
'auth': {
|
||
'session_id': self.session_id
|
||
}
|
||
}
|
||
|
||
ret = self.session.post('https://emp.mos.ru/v1.1/epd/get',
|
||
params={'token': self.token},
|
||
headers=wheaders,
|
||
verify=self.verify,
|
||
timeout=self.timeout,
|
||
json=wcrequest)
|
||
|
||
response = ret.json()
|
||
self.raise_for_status(response)
|
||
return response['result']
|
||
|
||
def get_car_fines(self, sts_number):
|
||
"""
|
||
:param sts_number: unicode string contains car sts_numer
|
||
:return:
|
||
{
|
||
"paid": [{
|
||
"seriesAndNumber": "xxxxx",
|
||
"date": "2018-08-30+03:00",
|
||
"offence_place": "МОСКВА Г. МКАД,xxxx, ВНЕШНЯЯ СТОРОНА",
|
||
"offenceType": "12.9ч.2 - Превышение установленной скорости движения транспортного средства на величину от 20 до 40 километров в час ",
|
||
"cost": "500",
|
||
"is_discount": false,
|
||
"drive_license": null,
|
||
"sts_number": "xxxxxxx",
|
||
"executionState": "Исполнено",
|
||
"is_fssp": false
|
||
}],
|
||
"unpaid": []
|
||
}
|
||
"""
|
||
assert self.session_id
|
||
wheaders = deepcopy(self.headers)
|
||
wheaders.update({
|
||
'X-Clears-tags': 'FORCE_NETWORK',
|
||
'X-Cache-ov-mode': 'DEFAULT',
|
||
'Content-Type': 'application/json; charset=UTF-8' })
|
||
|
||
wcrequest = {
|
||
'sts_number': sts_number,
|
||
'info': {
|
||
'guid': self.guid
|
||
},
|
||
'auth': {
|
||
'session_id': self.session_id
|
||
}
|
||
}
|
||
|
||
ret = self.session.post('https://emp.mos.ru/v1.0/offence/getOffence',
|
||
params={'token': self.token},
|
||
headers=wheaders,
|
||
verify=self.verify,
|
||
timeout=self.timeout,
|
||
json=wcrequest)
|
||
|
||
response = ret.json()
|
||
self.raise_for_status(response)
|
||
return response['result']
|
||
|
||
def logout(self, timeout=None):
|
||
"""
|
||
Почему то очень долго выполняется (5 сек)
|
||
"""
|
||
if self.session_id:
|
||
logout_data = {
|
||
'info': {
|
||
'guid': self.guid
|
||
},
|
||
'auth': {
|
||
'session_id': self.session_id
|
||
}
|
||
}
|
||
ret = self.session.post('https://emp.mos.ru/v1.0/auth/logout',
|
||
params={'token': self.token},
|
||
headers=self.headers,
|
||
timeout=timeout or self.timeout,
|
||
json=logout_data)
|
||
|
||
response = ret.json()
|
||
self.raise_for_status(response)
|
||
self.session_id = None
|
||
return response['result']
|
||
|
||
|
||
class MosAPI(object):
|
||
"""
|
||
kwargs:
|
||
|
||
token: уникальный ключ приложения
|
||
guid: некий уникальный ключ
|
||
https_verify: ключ verify в GET, POST запросах, по умолчанию 'False'
|
||
timeout: ключ timeout в GET, POST запросах
|
||
user_agent: версия веб клиента
|
||
dev_user_agent: 'Android' для ОС Android
|
||
dev_app_version: версия ОС
|
||
"""
|
||
|
||
def __init__(self, **kwargs):
|
||
self.kwargs = kwargs
|
||
self._clients = {'default': Client(**kwargs)}
|
||
|
||
def client(self, client_id='default', **kwargs):
|
||
if client_id and \
|
||
client_id not in self._clients:
|
||
k = self.kwargs
|
||
k.update(kwargs)
|
||
self._clients[client_id] = Client(**k)
|
||
return self._clients[client_id]
|
||
|
||
# if only one client
|
||
def is_active(self):
|
||
return self.client().is_active()
|
||
def login(self, *args):
|
||
return self.client().login(*args)
|
||
def logout(self, *args):
|
||
return self.client().logout(*args)
|
||
|
||
def get_profile(self):
|
||
return self.client().get_profile()
|
||
def get_flats(self):
|
||
return self.client().get_flats()
|
||
def get_watercounters(self, *args):
|
||
return self.client().get_watercounters(*args)
|
||
def send_watercounters(self, *args):
|
||
return self.client().send_watercounters(*args)
|
||
def get_electrocounters(self, *args):
|
||
return self.client().get_electrocounters(*args)
|
||
def send_electrocounters(self, *args):
|
||
return self.client().send_electrocounters(*args)
|
||
def get_epd(self, *args):
|
||
return self.client().get_epd(*args)
|
||
def get_car_fines(self, *args):
|
||
return self.client().get_car_fines(*args)
|
||
|
||
class Water():
|
||
COLD = 1
|
||
HOT = 2
|
||
|
||
@staticmethod
|
||
def water_abbr(water):
|
||
if water == Water.COLD: return u'ХВС'
|
||
elif water == Water.HOT: return u'ГВС'
|
||
|
||
@staticmethod
|
||
def name(water):
|
||
if water == Water.COLD: return u'холодная вода'
|
||
elif water == Water.HOT: return u'горячая вода'
|
||
|
||
|
||
class Watercounter():
|
||
"""
|
||
[{ 'counterId': 1437373, # внутреннее id счетчика
|
||
'type': 1, # тип воды
|
||
'num': '417944', # серийный номер счетчика
|
||
'checkup': '2023-09-25+03:00', # дата следующей поверки
|
||
'indications':
|
||
[{'period': '2018-08-31+03:00', 'indication': '21.38'},
|
||
{'period': '2018-07-31+03:00', 'indication': '20.7'},
|
||
{'period': '2018-06-30+03:00', 'indication': '19'}]
|
||
},
|
||
{...}
|
||
]
|
||
|
||
list(filter(lambda x: x['num'] == num, response['counters']))
|
||
list(filter(lambda x: x['counterId'] == id, response['counters']))
|
||
list(filter(lambda x: x['type'] == water_type_id, response['counters']))
|
||
"""
|
||
|
||
@staticmethod
|
||
def last_value(counter):
|
||
"""
|
||
alphabetical sort data =)
|
||
:param counter: counter JSON
|
||
:return: float or None
|
||
"""
|
||
indications = counter['indications']
|
||
indications.sort(key=lambda x: x['period'])
|
||
if indications:
|
||
return float(indications[-1]['indication'])
|
||
|
||
@staticmethod
|
||
def water_title(counter):
|
||
return Watercounter.humanreadable_name(counter['type'])
|
||
|
||
@staticmethod
|
||
def checkup(counter):
|
||
"""
|
||
https://docs.python.org/3/library/datetime.html#datetime.timezone
|
||
Если нужно UTC d.replace(tzinfo=None)
|
||
|
||
:param counter: counter JSON
|
||
:return: datetime с временной зоной
|
||
"""
|
||
checkup = counter['checkup']
|
||
d = datetime.strptime(checkup, '%Y-%m-%d%z') #'checkup': '2023-09-25+03:00'
|
||
return d
|
||
|
||
@staticmethod
|
||
def serialize_for_send(counter, value):
|
||
"""
|
||
:param counterId: id счетчика. не номер из приложения!
|
||
:param value: значение float
|
||
:return: dict
|
||
"""
|
||
return {
|
||
'counter_id': int(counter['counterId']),
|
||
'period': datetime.now().strftime("%Y-%m-%d"),
|
||
'indication': '{:.2f}'.format(value).replace('.', ',')
|
||
}
|