Init
This commit is contained in:
@@ -0,0 +1,628 @@
|
||||
# -*- 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('.', ',')
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
import requests
|
||||
import hashlib
|
||||
import globals
|
||||
import datetime as dt
|
||||
from xml.etree import ElementTree
|
||||
from automation import Automation, Base # type: ignore
|
||||
|
||||
class MosEnergoSbytSender(Automation):
|
||||
def initialize(self):
|
||||
super().initialize()
|
||||
self.run_daily(self.send_mosenergosbyt_counters, dt.time(12, 0, 0))
|
||||
# self.send_mosenergosbyt_counters({})
|
||||
|
||||
def send_mosenergosbyt_counters(self, kwargs):
|
||||
if not 'home_power_total' in self.entity_ids:
|
||||
self.error('Please define home_power_total in entity_ids.')
|
||||
return
|
||||
|
||||
current_counter_value = int(float(self.get_state(self.entity_ids['home_power_total'])))
|
||||
self.log('Sending current counter value: {}'.format(current_counter_value))
|
||||
self.app.send(current_counter_value)
|
||||
|
||||
|
||||
class MosEnergoSbytUpdater(Automation):
|
||||
def initialize(self):
|
||||
super().initialize()
|
||||
self.timer = self.run_every(self.update_sensors, self.datetime(), 1*60*60)
|
||||
# self.send_mosenergosbyt_counters({})
|
||||
|
||||
def update_sensors(self, args) -> None:
|
||||
self.app.update()
|
||||
counter_value = self.app.value
|
||||
counter_date = self.app.date
|
||||
balance = self.app.balance
|
||||
self.log('Current counter value is: {} (@{}), balance is: {}'.format(counter_value, counter_date, balance))
|
||||
|
||||
if 'mosenergosbyt_total' in self.entity_ids:
|
||||
entity_id = self.entity_ids['mosenergosbyt_total']
|
||||
attributes = {
|
||||
'unit_of_measurement': 'kWh',
|
||||
'date': counter_date,
|
||||
# 'checkup': checkup,
|
||||
'icon': 'mdi:av-timer',
|
||||
'friendly_name': 'Счетчик (мосэнергосбыт)',
|
||||
'description': 'Created and updated from appdaemon ({})'.format(__name__)
|
||||
}
|
||||
self.set_state(entity_id, state = counter_value, attributes = attributes)
|
||||
|
||||
if 'mosenergosbyt_balance' in self.entity_ids:
|
||||
entity_id = self.entity_ids['mosenergosbyt_balance']
|
||||
attributes = {
|
||||
'unit_of_measurement': '₽',
|
||||
'date': counter_date,
|
||||
'icon': 'mdi:bank-transfer',
|
||||
'friendly_name': 'Баланс (мосэнергосбыт)',
|
||||
'description': 'Created and updated from appdaemon ({})'.format(__name__)
|
||||
}
|
||||
self.set_state(entity_id, state = balance, attributes = attributes)
|
||||
|
||||
class MosEnergoSbytCounter(Base):
|
||||
_version = 11
|
||||
_device = "ios/12.1.2/2.14.14(221)"
|
||||
_session = None
|
||||
_profile = None
|
||||
|
||||
# def initialize(self):
|
||||
# super().initialize()
|
||||
|
||||
@property
|
||||
def value(self) -> float:
|
||||
"""Return the current state of counter."""
|
||||
if (self._profile == None):
|
||||
self.update()
|
||||
return float(self._profile['SH_POK_STR'])
|
||||
|
||||
@property
|
||||
def balance(self) -> float:
|
||||
"""Return the current state of counter."""
|
||||
if (self._profile == None):
|
||||
self.update()
|
||||
balance = -1 * float(self._profile['SM_DEBT'])
|
||||
if balance == -0:
|
||||
balance = 0
|
||||
return balance
|
||||
|
||||
@property
|
||||
def date(self) -> str:
|
||||
"""Return the current state of counter."""
|
||||
if (self._profile == None):
|
||||
self.update()
|
||||
return self._profile['DT_POK']
|
||||
|
||||
def is_active(self):
|
||||
"""
|
||||
:return: True если уже залогинился
|
||||
"""
|
||||
return self._session is not None
|
||||
|
||||
def update(self) -> None:
|
||||
if not self.is_active():
|
||||
self._session = self._get_session()
|
||||
url = 'https://lkkbyt.mosenergosbyt.ru/gate/do?api_version={}&id_session={}&process=refresh&type=1'.format(self._version, self._session)
|
||||
response = requests.get(url).text
|
||||
values_response = self._parse_response(response)
|
||||
self._profile = values_response
|
||||
# self.log(values_response)
|
||||
|
||||
def send(self, value) -> bool:
|
||||
if not self.is_active():
|
||||
self._session = self._get_session()
|
||||
|
||||
url = 'https://lkkbyt.mosenergosbyt.ru/gate/do?id_session={}&pr_check_ras=0&pr_flat_pu=0&process=dotranspok&type=3&vl_pok_t1={}&vl_pok_t2=0&vl_pok_t3=0'.format(self._session, value)
|
||||
response = requests.get(url).text
|
||||
send_response = self._parse_response(response)
|
||||
result = send_response['KD_RESULT']
|
||||
if result == 0:
|
||||
return True
|
||||
return False
|
||||
|
||||
def _get_session(self) -> str:
|
||||
login = str(self.properties['login'])
|
||||
pass_md5 = hashlib.md5(self.properties['pwd'].encode('utf-8')).hexdigest()
|
||||
ls = ''
|
||||
phone = ''
|
||||
if len(login) == 10:
|
||||
ls = login
|
||||
elif len(login) == 11:
|
||||
phone = login
|
||||
else:
|
||||
self.error('unknown login type: {}'.format(login))
|
||||
return
|
||||
url = "https://lkkbyt.mosenergosbyt.ru/gate/do?api_version={}&id_device={}&phone={}&ls={}&process=login&pw_abn={}&type=1".format(self._version, self._device, phone, ls, pass_md5)
|
||||
response = requests.get(url).text
|
||||
session_response = self._parse_response(response)
|
||||
session = session_response['ID_SESSION']
|
||||
return session
|
||||
|
||||
def _parse_response(self, response):
|
||||
et = ElementTree.fromstring(response)
|
||||
|
||||
headers = []
|
||||
values = []
|
||||
for node in et.findall('./h/p'):
|
||||
headers.append(node.text)
|
||||
for node in et.findall('./b/p'):
|
||||
values.append(node.text)
|
||||
result = {}
|
||||
for i in range(0, len(headers)):
|
||||
header = headers[i]
|
||||
value = values[i]
|
||||
result[header] = value
|
||||
return result
|
||||
@@ -0,0 +1,47 @@
|
||||
mosenergo_counter:
|
||||
module: mosenergosbyt
|
||||
class: MosEnergoSbytCounter
|
||||
properties:
|
||||
login: !secret mosenergo_main_login
|
||||
pwd: !secret mosenergo_main_pwd
|
||||
|
||||
mosenergo_counter_updater:
|
||||
module: mosenergosbyt
|
||||
class: MosEnergoSbytUpdater
|
||||
entity_ids:
|
||||
mosenergosbyt_total: sensor.power_mosenergosbyt_total
|
||||
mosenergosbyt_balance: sensor.power_mosenergosbyt_balance
|
||||
app: mosenergo_counter
|
||||
dependencies:
|
||||
- mosenergo_counter
|
||||
global_dependencies:
|
||||
- globals
|
||||
|
||||
mosenergo_counter_sender:
|
||||
module: mosenergosbyt
|
||||
class: MosEnergoSbytSender
|
||||
entity_ids:
|
||||
home_power_total: sensor.power_main_total
|
||||
app: mosenergo_counter
|
||||
dependencies:
|
||||
- mosenergo_counter
|
||||
global_dependencies:
|
||||
- globals
|
||||
|
||||
|
||||
mosenergo_alt:
|
||||
module: mosenergosbyt
|
||||
class: MosEnergoSbytCounter
|
||||
properties:
|
||||
login: !secret mosenergo_alt_login
|
||||
pwd: !secret mosenergo_alt_pwd
|
||||
|
||||
mosenergo_alt_updater:
|
||||
module: mosenergosbyt
|
||||
class: MosEnergoSbytUpdater
|
||||
app: mosenergo_alt
|
||||
dependencies:
|
||||
- automation
|
||||
- mosenergo_alt
|
||||
global_dependencies:
|
||||
- globals
|
||||
@@ -0,0 +1,281 @@
|
||||
import globals
|
||||
import datetime as dt
|
||||
from automation import Automation, Base
|
||||
from datetime import datetime, tzinfo, timedelta
|
||||
from mos_lib import MosAPI, Watercounter, Water, EmpServerException
|
||||
|
||||
class WaterCounterSender(Automation):
|
||||
timezone = '+03:00'
|
||||
def initialize(self):
|
||||
super().initialize()
|
||||
self.run_daily(self.send_mosru_counters, dt.time(12, 00, 0))
|
||||
|
||||
def send_mosru_counters(self, kwargs):
|
||||
self.log('Starting sending counters value to mos.ru')
|
||||
new_values = []
|
||||
self.app.update_watercounters()
|
||||
water_hot = self.app.water_hot
|
||||
water_cold = self.app.water_cold
|
||||
counters = [water_hot, water_cold]
|
||||
|
||||
for counter in counters:
|
||||
counter_type = counter['type']
|
||||
|
||||
checkup = counter['checkup'].replace(self.timezone,'')
|
||||
checkup_date = datetime.strptime(checkup, '%Y-%m-%d')
|
||||
|
||||
# подходит дата поверки
|
||||
if checkup_date < datetime.now():
|
||||
self.error('Counter {} checkup date is expired at {}!'.format(counter['counterId'], checkup))
|
||||
continue
|
||||
|
||||
our_sensor = ""
|
||||
if counter_type == 1:
|
||||
our_sensor = self.entity_ids['cold']
|
||||
elif counter_type == 2:
|
||||
our_sensor = self.entity_ids['hot']
|
||||
real_value = int(float(self.get_state(our_sensor)))
|
||||
self.log("Updating counter {} ({}) to real value: {}".format(counter['counterId'], our_sensor, real_value))
|
||||
new_values.append(Watercounter.serialize_for_send(counter, real_value))
|
||||
|
||||
if new_values:
|
||||
try:
|
||||
self.app.send_watercounters(new_values)
|
||||
self.log('Counters values was sent to mos.ru')
|
||||
except EmpServerException as err:
|
||||
self.log('error sending: {}'.format(err))
|
||||
|
||||
|
||||
class WaterCounterSensor(Automation):
|
||||
timezone = '+03:00'
|
||||
def initialize(self):
|
||||
super().initialize()
|
||||
self.timer = self.run_every(self.update_sensors, self.datetime(), 1*60*60)
|
||||
|
||||
def update_sensors(self, args) -> None:
|
||||
self.log('Updating water counters')
|
||||
self.app.update_watercounters()
|
||||
if 'hot' in self.entity_ids:
|
||||
water_hot = self.app.water_hot
|
||||
self.create_water_entity(self.entity_ids['hot'], water_hot)
|
||||
if 'cold' in self.entity_ids:
|
||||
water_cold = self.app.water_cold
|
||||
self.create_water_entity(self.entity_ids['cold'], water_cold)
|
||||
|
||||
def create_water_entity(self, entity_id, counter):
|
||||
indication_max = 0
|
||||
indication_date = ""
|
||||
for indication in counter['indications']:
|
||||
current = float(indication['indication'])
|
||||
if (current > indication_max):
|
||||
indication_max = current
|
||||
indication_date = indication['period']
|
||||
indication_date = indication_date.replace(self.timezone,'')
|
||||
|
||||
counter_type = counter['type']
|
||||
checkup = counter['checkup']
|
||||
type_desc = ""
|
||||
type_desc_ru = ""
|
||||
if counter_type == 1:
|
||||
type_desc = "cold"
|
||||
type_desc_ru = "Холодная"
|
||||
elif counter_type == 2:
|
||||
type_desc = "hot"
|
||||
type_desc_ru = "Горячая"
|
||||
|
||||
icon = "mdi:av-timer"
|
||||
checkup = checkup.replace(self.timezone,'')
|
||||
checkup_date = datetime.strptime(checkup, '%Y-%m-%d')
|
||||
|
||||
# подходит дата поверки
|
||||
if (checkup_date - timedelta(days=31)) < datetime.now():
|
||||
icon = "mdi:alert-octagon"
|
||||
|
||||
attributes = {}
|
||||
attributes['unit_of_measurement'] = 'm3'
|
||||
attributes['date'] = indication_date
|
||||
attributes['checkup'] = checkup
|
||||
attributes['icon'] = icon
|
||||
attributes['friendly_name'] = "{}(mos.ru)".format(type_desc_ru)
|
||||
attributes['description'] = 'Created and updated from appdaemon ({})'.format(__name__)
|
||||
|
||||
self.set_state(entity_id, state = indication_max, attributes = attributes)
|
||||
|
||||
class EpdBalanceSensor(Automation):
|
||||
def initialize(self):
|
||||
super().initialize()
|
||||
self.timer = self.run_every(self.update_sensors, self.datetime(), 1*60*60)
|
||||
|
||||
def update_sensors(self, args) -> None:
|
||||
if 'epd_balance' in self.entity_ids:
|
||||
self.app.update_epd()
|
||||
epd_balance = self.app.epd_balance
|
||||
epd_description = self.app.epd_description
|
||||
self.log('Current epd balance: {}'.format(epd_balance))
|
||||
|
||||
entity_id = self.entity_ids['epd_balance']
|
||||
attributes = {
|
||||
'unit_of_measurement': '₽',
|
||||
'icon': 'mdi:bank-transfer',
|
||||
'date': datetime.now().strftime('%d.%m.%Y'),
|
||||
'unpaid': epd_description,
|
||||
'friendly_name': 'Баланс (ЕПД)',
|
||||
'description': 'Created and updated from appdaemon ({})'.format(__name__)
|
||||
}
|
||||
self.set_state(entity_id, state = epd_balance, attributes = attributes)
|
||||
|
||||
|
||||
class MosruBase(Base):
|
||||
_api = None
|
||||
_flat_id = None
|
||||
_guid='4421a3fb0567714b27dec84d3c3862dc'
|
||||
_app_version = '3.5.0.195 (102)'
|
||||
_user_agent = 'okhttp/3.8.1'
|
||||
_dev_user_agent = 'Android'
|
||||
_timezone = '+03:00'
|
||||
|
||||
def initialize(self):
|
||||
super().initialize()
|
||||
if not globals.check_properties(self,['login', 'pwd', 'token', 'paycode']):
|
||||
return
|
||||
self._api = MosAPI(token=self.properties['token'],
|
||||
user_agent=self._user_agent,
|
||||
guid=self._guid,
|
||||
dev_user_agent=self._dev_user_agent,
|
||||
dev_app_version=self._app_version,
|
||||
timeout=6)
|
||||
self._set_flat_id()
|
||||
|
||||
def _set_flat_id(self):
|
||||
''' Set flat id by properties or pay code '''
|
||||
if 'flat_id' in self.properties:
|
||||
self._flat_id = self.properties['flat_id']
|
||||
return
|
||||
if (self._flat_id != None):
|
||||
return
|
||||
self._check_login()
|
||||
flats = self._api.get_flats()
|
||||
for flat in flats:
|
||||
if (int(flat['paycode']) != self.properties['paycode']):
|
||||
continue
|
||||
self._flat_id = flat['flat_id']
|
||||
|
||||
def _check_login(self):
|
||||
if not self._api.is_active():
|
||||
self._api.login(self.properties['login'], self.properties['pwd'])
|
||||
|
||||
class MosruPower(MosruBase):
|
||||
_power_counters = None
|
||||
|
||||
@property
|
||||
def power_total(self):
|
||||
if (self._power_counters == None):
|
||||
self.update_power()
|
||||
return self._power_counters['zones'][0]['value']
|
||||
|
||||
@property
|
||||
def power_balance(self):
|
||||
if (self._power_counters == None):
|
||||
self.update_power()
|
||||
return float(self._power_counters['balance'])
|
||||
|
||||
def update_power(self) -> None:
|
||||
self._check_login()
|
||||
self._power_counters = self._api.get_electrocounters(self._flat_id)
|
||||
|
||||
class MosruWater(MosruBase):
|
||||
_water_counters = None
|
||||
|
||||
@property
|
||||
def water_hot(self):
|
||||
"""Return the current state of counter."""
|
||||
if (self._water_counters == None):
|
||||
self.update_watercounters()
|
||||
hots_json = list(filter(lambda x: x['type'] == Water.HOT, self._water_counters))
|
||||
return hots_json[0]
|
||||
|
||||
@property
|
||||
def water_cold(self):
|
||||
"""Return the current state of counter."""
|
||||
if (self._water_counters == None):
|
||||
self.update_watercounters()
|
||||
colds_json = list(filter(lambda x: x['type'] == Water.COLD, self._water_counters))
|
||||
return colds_json[0]
|
||||
|
||||
@property
|
||||
def water_hot_value(self):
|
||||
hot_value = Watercounter.last_value(self.water_hot)
|
||||
return hot_value
|
||||
|
||||
@property
|
||||
def water_cold_value(self):
|
||||
cold_value = Watercounter.last_value(self.water_cold)
|
||||
return cold_value
|
||||
|
||||
|
||||
def send_watercounters(self, new_values) -> None:
|
||||
self._check_login()
|
||||
self._api.send_watercounters(self._flat_id, new_values)
|
||||
|
||||
def update_watercounters(self) -> None:
|
||||
self._check_login()
|
||||
self._water_counters = self._api.get_watercounters(self._flat_id)['counters']
|
||||
|
||||
class MosruEpd(MosruBase):
|
||||
_epd_data = None
|
||||
|
||||
@property
|
||||
def epd_description(self) -> str:
|
||||
if (self._epd_data == None):
|
||||
self.update_epd()
|
||||
|
||||
unpaid = []
|
||||
for date in self._epd_data:
|
||||
epd = self._epd_data[date]
|
||||
if epd['is_paid']:
|
||||
continue
|
||||
unpaid.append('{}: {}'.format(date, epd['amount']))
|
||||
return ", ".join(unpaid)
|
||||
|
||||
|
||||
@property
|
||||
def epd_balance(self) -> float:
|
||||
"""Return the current state of counter."""
|
||||
if (self._epd_data == None):
|
||||
self.update_epd()
|
||||
balance = 0
|
||||
for date in self._epd_data:
|
||||
epd = self._epd_data[date]
|
||||
epd_is_paid = epd['is_paid']
|
||||
if epd_is_paid:
|
||||
continue
|
||||
balance = balance - epd['amount']
|
||||
return balance
|
||||
|
||||
def update_epd(self) -> None:
|
||||
self._check_login()
|
||||
data = {}
|
||||
date = datetime.now().date()
|
||||
for i in range (0,6):
|
||||
process_date = date - timedelta(i*365/12)
|
||||
date_str = process_date.strftime('%d.%m.%Y')
|
||||
epd = self._api.get_epd(self._flat_id, date_str, False)
|
||||
for i in range(0, 10): #retries
|
||||
if epd != []:
|
||||
break
|
||||
epd = self._api.get_epd(self._flat_id, date_str, False)
|
||||
epd_first = epd[0]
|
||||
data[date_str] = epd_first
|
||||
# epd_total = epd_first['amount']
|
||||
# epd_is_paid = epd_first['is_paid']
|
||||
# self.log(" - Дата: {}, сумма: {}, оплачен: {}.".format(date_str, epd_total, epd_is_paid))
|
||||
self._epd_data = data
|
||||
|
||||
class MosruClient(MosruEpd, MosruWater, MosruPower):
|
||||
def initialize(self):
|
||||
super().initialize()
|
||||
# self.log('Flat id is: {}'.format(self._flat_id))
|
||||
# self.log('epd balance: {}'.format(self.epd_balance))
|
||||
# self.log('cold: {}'.format(self.water_cold))
|
||||
# self.log('hot: {}'.format(self.water_hot))
|
||||
# self.log('power_counter: {}, balance: {}'.format(self.power_total, self.power_balance))
|
||||
@@ -0,0 +1,50 @@
|
||||
#################################
|
||||
# MOS.RU DATA RETRIEVER #
|
||||
#################################
|
||||
mosru_client:
|
||||
class: MosruClient
|
||||
module: mosru
|
||||
properties:
|
||||
login: !secret mosru_login
|
||||
pwd: !secret mosru_pwd
|
||||
token: !secret mosru_token
|
||||
paycode: !secret mosru_paycode
|
||||
flat_id: !secret mosru_flat_id
|
||||
global_dependencies:
|
||||
- globals
|
||||
|
||||
epd_sensor:
|
||||
class: EpdBalanceSensor
|
||||
module: mosru
|
||||
entity_ids:
|
||||
epd_balance: sensor.epd_balance
|
||||
app: mosru_client
|
||||
dependencies:
|
||||
- mosru_client
|
||||
global_dependencies:
|
||||
- globals
|
||||
|
||||
water_sensor:
|
||||
class: WaterCounterSensor
|
||||
module: mosru
|
||||
entity_ids:
|
||||
hot: sensor.counter_mosru_hot
|
||||
cold: sensor.counter_mosru_cold
|
||||
app: mosru_client
|
||||
dependencies:
|
||||
- mosru_client
|
||||
global_dependencies:
|
||||
- globals
|
||||
|
||||
water_sender:
|
||||
class: WaterCounterSender
|
||||
module: mosru
|
||||
entity_ids:
|
||||
hot: sensor.counter_hot
|
||||
cold: sensor.counter_cold
|
||||
app: mosru_client
|
||||
dependencies:
|
||||
- mosru_client
|
||||
global_dependencies:
|
||||
- globals
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import appdaemon.plugins.hass.hassapi as hass
|
||||
|
||||
#
|
||||
# Water leak detector
|
||||
# close water valve and send a report
|
||||
#
|
||||
# Args:
|
||||
#
|
||||
# sensor = water leak sensor group
|
||||
# valve = water valve - app will close it, when leak is detected
|
||||
# notify = notify name to send notification
|
||||
#
|
||||
# None
|
||||
#
|
||||
# Release Notes
|
||||
#
|
||||
# Version 1.0:
|
||||
# Initial Version
|
||||
|
||||
class WaterLeak(hass.Hass):
|
||||
ringtone = 2
|
||||
gw_mac = ''
|
||||
def initialize(self):
|
||||
if 'sensor' not in self.args or 'valve' not in self.args or 'notify' not in self.args:
|
||||
self.error("Please provide sensor, valve and notify in config!")
|
||||
return
|
||||
self.listen_state(self.water_leak, self.args['sensor'])
|
||||
|
||||
def water_leak(self, entity, attribute, old, new, kwargs):
|
||||
if new == "on":
|
||||
self.send_alarm()
|
||||
|
||||
def send_alarm(self):
|
||||
if 'constraint' in self.args and not self.constrain_input_boolean(self.args['constraint']):
|
||||
return
|
||||
|
||||
#close water valve
|
||||
self.turn_off(self.args['valve'])
|
||||
## to ensure that water is closed. It is important!!!
|
||||
self.call_service("mqtt/publish", topic = 'home/bath/bath/valve/set', payload = 'close')
|
||||
self.call_service("xiaomi_aqara/play_ringtone", gw_mac=self.gw_mac, ringtone_id=self.ringtone, ringtone_vol=100)
|
||||
|
||||
message = "ВНИМАНИЕ! В ваше отстутствие сработал датчик протечки воды!!! Выключаю подачу воды."
|
||||
self.notify(message, name = self.args['notify'])
|
||||
|
||||
message = "Состояние датчиков протечки:\n"
|
||||
attributes = self.get_state(self.args['sensor'], attribute = "all")
|
||||
if 'attributes' in attributes:
|
||||
attributes = attributes['attributes']
|
||||
if 'entity_id' in attributes:
|
||||
for entity_id in attributes["entity_id"]:
|
||||
message += '{} ({}) = {}\n'.format(self.friendly_name(entity_id), entity_id, self.get_state(entity_id))
|
||||
self.notify(message, name = self.args['notify'])
|
||||
@@ -0,0 +1,13 @@
|
||||
#################################
|
||||
# WATER LEAK #
|
||||
#################################
|
||||
water_leak:
|
||||
module: water_leak
|
||||
class: WaterLeak
|
||||
constraint: input_boolean.water_leak_control
|
||||
sensor: group.water_leak
|
||||
valve: switch.water_valve
|
||||
notify: telegram
|
||||
gw_mac: !secret aqara1_mac
|
||||
global_dependencies:
|
||||
- globals
|
||||
Reference in New Issue
Block a user