diff --git a/docs/dev.md b/docs/dev.md
index ae4893a..c6c73e6 100644
--- a/docs/dev.md
+++ b/docs/dev.md
@@ -1,25 +1,286 @@
+>Перед началом обязательно, хотя бы поверхностно, ознакомиться с документацией к
+[Django](https://docs.djangoproject.com).
+
## Добавление поддерживаемого устройства (Свича)
+Для того чтоб добавить новый тип устройства с которым потом сможет работать биллинг нужно открыть файл *devapp/dev_types.py*
+и переопределить 2 интерфейса. Первый это *BasePort* для порта свича, а второй *DevBase* для самого свича соответственно.
+
+Разберём этот процесс на примере готовой реализации для Eltex.
+
+```python
+class EltexPort(BasePort):
+
+ def __init__(self, snmpWorker, *args, **kwargs):
+ BasePort.__init__(self, *args, **kwargs)
+ assert issubclass(snmpWorker.__class__, SNMPBaseWorker)
+ self.snmp_worker = snmpWorker
+
+ # выключаем этот порт
+ def disable(self):
+ self.snmp_worker.set_int_value(
+ "%s.%d" % ('.1.3.6.1.2.1.2.2.1.7', self.num),
+ 2
+ )
+
+ # включаем этот порт
+ def enable(self):
+ self.snmp_worker.set_int_value(
+ "%s.%d" % ('.1.3.6.1.2.1.2.2.1.7', self.num),
+ 1
+ )
+```
+Тут в инициилизации мы передаём все базовые параметры базовому конструктору, и дополнительный аргумент snmpWorker
+для работы по SNMP. *snmpWorker* это объект реализованного интерфейса SNMPBaseWorker, далее я опишу где мы его реализуем.
+Для порта надо переопределить 2 метода: *disable* и *enable* понятно для чего, чтоб включать и отключать порт.
+
+Шаблон реализации можно даже не менять, просто укажите вместо строки .1.3.6.1.2.1.2.2.1.7 нужный SNMP OID для включения порта.
+К этой строке будет добавляться номер порта который нужно включить.
+Для отключения так-же по аналогии.
+
+Теперь реализация для свича:
+```python
+class EltexSwitch(DLinkDevice):
+
+ @staticmethod
+ def description():
+ return _('Eltex switch')
+
+ def get_ports(self):
+ #nams = self.get_list('.1.3.6.1.4.1.171.10.134.2.1.1.100.2.1.3')
+ stats = self.get_list('.1.3.6.1.2.1.2.2.1.7')
+ oper_stats = self.get_list('.1.3.6.1.2.1.2.2.1.8')
+ #macs = self.get_list('.1.3.6.1.2.1.2.2.1.6')
+ speeds = self.get_list('.1.3.6.1.2.1.31.1.1.1.15')
+ res = []
+ for n in range(28):
+ res.append(EltexPort(self,
+ n+1,
+ '',#nams[n] if len(nams) > 0 else _('does not fetch the name'),
+ True if int(stats[n]) == 1 else False,
+ '',#macs[n] if len(macs) > 0 else _('does not fetch the mac'),
+ int(speeds[n]) if len(speeds) > 0 and int(oper_stats[n]) == 1 else 0,
+ ))
+ return res
+
+ def get_device_name(self):
+ return self.get_item('.1.3.6.1.2.1.1.5.0')
+
+ def uptime(self):
+ uptimestamp = safe_int(self.get_item('.1.3.6.1.2.1.1.3.0'))
+ tm = RuTimedelta(timedelta(seconds=uptimestamp/100)) or RuTimedelta(timedelta())
+ return tm
+
+ @staticmethod
+ def has_attachable_to_subscriber():
+ return False
+
+ @staticmethod
+ def is_use_device_port():
+ return False
+```
+Метод **@description** Просто отображает человекопонятное название вашего устройства в биллинге.
+Заметьте что строка на английском и заключена в процедуру **_** (это ugettext_lazy, см. в импорте вверху файла),
+это локализация для текущего языка. Про локализацию можно почитать в соответствующем разделе [django translation](https://docs.djangoproject.com/en/1.9/topics/i18n/translation/).
+
+Метод **@get_ports** чаще всего редко изменяется по алгоритму, так что вам, в большенстве случаев, достаточно добавить
+нужные SNMP OID в соответствующие места процедуры. Но вы вольны реализовать ваш метод получения портов
+как вам угодно, главное чтоб возвращался список объектов определённого выше класса порта для этого свича.
+В данном случае возвращается список объектов *EltexPort*.
+
+Метод **@get_device_name** получает по SNMP имя устройства, просто укажите в вашей реализации нужный OID.
+
+Метод **@uptime**, понятно что возвращает, укажите нужный OID. Вернётся тип *RuTimedelta*, это не тип Django, я сам его реализовал
+для локализации временного промежутка на русский.
+
+Статический метод **@has_attachable_to_subscriber** возвращает правду если это устройство можно привязать к абоненту.
+Например у Dlink стоит True потому что Dlink стоит во многих местах на доступе, и его порты принадлежат
+абонентам при авторизации.
+
+Статический метод **@is_use_device_port** используется в DHCP чтоб понять что мы используем для привязки к абоненту всё устройство или
+только порт устройства. Например, если у устройства только 1 порт абонента (PON ONU), то нужно вернуть True, во всех остальных случаях False.
+
+Реализация SNMPBaseWorker по сути не нужна, класс абстрактных методов не имеет.
+Потому когда наследуем наследуемся от *DevBase* то в базовые классы добавим и SNMPBaseWorker, как это сделано в *DLinkDevice*:
+```python
+class DLinkDevice(DevBase, SNMPBaseWorker):
+
+ def __init__(self, ip, snmp_community, ver=2):
+ DevBase.__init__(self)
+ SNMPBaseWorker.__init__(self, ip, snmp_community, ver)
+```
+А далее просто передадим параметры для конструкторов обоих базовых классов.
+
+Вы, наверное, обратили внимание, что *EltexSwitch* наследован от *DLinkDevice*, это потому что некоторые методы идентичны,
+и реализация для обоих свичей похожа.
+
+>П.С. Не изучайте как пример реализацию для PON, она, как по мне, костыльна. Это связано с тем что PON сильно отличается от
+>принципа работы обычного свича, и чтоб подружить свичи и PON был реализован такой костыль.
## Добавим платёжную систему
-Для того чтоб добавить платёжную систему добавьте в файл *pay_systems* каталога abonapp
-процедуру которая будет принимать request, далее он пригодится в теле вашей процедуры.
-Пустая процедура, возвращающая xml, будет выглядеть так:
+Для того чтоб добавить платёжную систему добавьте в файл *abonapp/pay_systems.py* процедуру которая будет принимать
+request, далее он пригодится в теле вашей процедуры. это тот самый request который передаётся в *view*. Пустая процедура, возвращающая xml, будет выглядеть так:
- def my_custom_pay_system(request):
- return "\n" \
- "Pay ok\n"
+```python
+def my_custom_pay_system(request):
+ return "\n" \
+ "Pay ok\n"
+```
Затем импортируйте её в процедуру *terminal_pay* в файле views.py каталога abonapp.
Для примера это будет выглядеть так:
- @atomic
- def terminal_pay(request):
- from .pay_systems import my_custom_pay_system
- ret_text = my_custom_pay_system(request)
- return HttpResponse(ret_text)
+```python
+@atomic
+def terminal_pay(request):
+ from .pay_systems import my_custom_pay_system
+ ret_text = my_custom_pay_system(request)
+ return HttpResponse(ret_text)
+```
-Проследите чтоб ваша процедура вы вызывала исключений, обрабатывайте всё внутри тела процедуры.
+Проследите чтоб ваша процедура не вызывала исключений, обрабатывайте всё внутри тела процедуры.
Про декоратор **@atomic** вы можете прочитать в документации к [Django](https://docs.djangoproject.com/en/1.9/topics/db/transactions).
В кратце этот декоратор защищает от незавешённых транзакций, например при высокой нагрузке.
+
+## Реализация своего NAS
+Сейчас биллинг работает с Mikrotik в роли устройства для доступа абонентов в интернет.
+Как можно реализовать такой-же для вашего роутера, например на GNU/Linux.
+
+Создадим файл *agent/mod_linux.py* и реализуем потомка для интерфейса *BaseTransmitter*.
+Методы вашего класса будут вызываться биллингом для взаимодействия с сервером доступа абонентов в интернет(NAS).
+
+```python
+from .core import BaseTransmitter, NasFailedResult, NasNetworkError
+
+class LinuxTransmitter(BaseTransmitter):
+
+ def add_user_range(self, user_list):
+ """добавляем список абонентов в NAS"""
+
+ @abstractmethod
+ @check_input_type(AbonStruct)
+ def remove_user_range(self, users):
+ """удаляем список абонентов"""
+
+ @abstractmethod
+ @check_input_type(AbonStruct)
+ def add_user(self, user, *args):
+ """добавляем абонента"""
+
+ @abstractmethod
+ @check_input_type(AbonStruct)
+ def remove_user(self, user):
+ """удаляем абонента"""
+
+ @abstractmethod
+ @check_input_type(AbonStruct)
+ def update_user(self, user, *args):
+ """
+ Чтоб обновить абонента можно изменить всё кроме его uid, по uid абонент будет найден.
+ Это значит что вы можете передать объект user класса AbonStruct, где только uid будет указывать на абонента,
+ а остальные поля будут содержать новое значение.
+ """
+
+ @abstractmethod
+ @check_input_type(TariffStruct)
+ def add_tariff_range(self, tariff_list):
+ """
+ Пока не используется, зарезервировано.
+ Добавляет список тарифов в NAS
+ """
+
+ @abstractmethod
+ @check_input_type(TariffStruct)
+ def remove_tariff_range(self, tariff_list):
+ """
+ Пока не используется, зарезервировано.
+ Удаляем список тарифов по уникальным идентификаторам
+ """
+
+ @abstractmethod
+ @check_input_type(TariffStruct)
+ def add_tariff(self, tariff):
+ """
+ Пока не используется, зарезервировано.
+ Добавляем тариф
+ """
+
+ @abstractmethod
+ @check_input_type(TariffStruct)
+ def update_tariff(self, tariff):
+ """
+ Пока не используется, зарезервировано.
+ Чтоб обновить тариф надо изменить всё кроме его tid, по tid тариф будет найден
+ """
+
+ @abstractmethod
+ @check_input_type(TariffStruct)
+ def remove_tariff(self, tid):
+ """
+ :param tid: id тарифа в среде NAS сервера чтоб удалить по этому номеру
+ Пока не используется, зарезервировано.
+ """
+
+ @abstractmethod
+ @check_input_type(TariffStruct)
+ def ping(self, host, count=10):
+ """
+ :param host: ip адрес в текстовом виде, например '192.168.0.1'
+ :param count: количество пингов
+ :return: None если не пингуется, иначе кортеж, в котором (сколько вернулось, сколько было отправлено)
+ """
+
+ @abstractmethod
+ def read_users(self):
+ """
+ Читаем пользователей с NAS
+ :return: список AbonStruct
+ """
+```
+
+Для того чтоб биллинг знал о вашем классе надо указать его в *agent/\_\_init\_\_.py*.
+Замените
+>from .mod_mikrotik import MikrotikTransmitter
+
+На это
+>from .mod_mikrotik import LinuxTransmitter
+
+И укажите ваш класс
+> Transmitter = MikrotikTransmitter
+
+Получится примерно такое содержимое:
+
+```python
+from .mod_mikrotik import LinuxTransmitter
+from .core import NasFailedResult, NasNetworkError
+from .structs import TariffStruct, AbonStruct
+
+Transmitter = LinuxTransmitter
+```
+
+Для примера, как вы наверное уже догадались, можно посмотреть реализацию для Mikrotik в файле *agent/mod_mikrotik.py*
+
+Чтобы выводить в биллинге различные сообщения об ошибках есть 2 типа исключений: *NasFailedResult* и *NasNetworkError*.
+NasNetworkError, как понятно из названия, вызывается при проблемах в сети. А NasFailedResult при ошибочных кодах возврата из модуля на сервере NAS.
+
+Биллинг прослушивает эти исключения при выполнении, и при возбуждении этих исключений отображает текст ошибки на экране пользователя.
+
+При переопределении базового класса пожалуйста не забывайте вызвать базовый метод чтоб отработали декораторы методов интерфейса, этот декоратор проверяет тип входных данных.
+Динамическая типизация python иногда подкладывает свинью в том смысле что можно передать не то что вы хотели бы передать, потому типы лучше проконтролировать, и тогда интерпретатор станет вашим другом помошником :)
+
+Когда я прошу вызвать базовый метод, я имею ввиду это:
+```python
+...
+def add_user_range(self, user_list):
+ super(LinuxTransmitter, self).add_user_range(user_list)
+ # ваш код
+...
+```
+
+Кстати, не все методы обязательно реализовывать, некоторые из них зарезервированы на будущие цели, в комментариях к их прототипам в интерфейсе *BaseTransmitter* это сказано.
+Поэтому просто переопределите эти зарезервированные методы как пустые, например метод *add_tariff_range* нигде в биллинге пока не вызывается. так что можно определить его пустым.
+```python
+def add_tariff_range(self, tariff_list):
+ pass
+```