Initial commit

This commit is contained in:
2025-07-31 17:00:53 +03:00
commit 582d0b1316
22 changed files with 2205178 additions and 0 deletions

8
.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

8
.idea/api.rsgrinko.ru.iml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

64
.idea/deployment.xml generated Normal file
View File

@@ -0,0 +1,64 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="PublishConfigData" autoUpload="Always" serverName="vm-web-02 (10.10.10.54)" remoteFilesAllowedToDisappearOnAutoupload="false">
<serverData>
<paths name="dev-web03.servicecloud.info">
<serverdata>
<mappings>
<mapping local="$PROJECT_DIR$" web="/" />
</mappings>
</serverdata>
</paths>
<paths name="dew-web04.servicecloud.info">
<serverdata>
<mappings>
<mapping local="$PROJECT_DIR$" web="/" />
</mappings>
</serverdata>
</paths>
<paths name="ip.it-stories.ru">
<serverdata>
<mappings>
<mapping local="$PROJECT_DIR$" web="/" />
</mappings>
</serverdata>
</paths>
<paths name="rsgrinko.online">
<serverdata>
<mappings>
<mapping local="$PROJECT_DIR$" web="/" />
</mappings>
</serverdata>
</paths>
<paths name="tula.it-stories.ru">
<serverdata>
<mappings>
<mapping local="$PROJECT_DIR$" web="/" />
</mappings>
</serverdata>
</paths>
<paths name="vm-web-02 (10.10.10.54)">
<serverdata>
<mappings>
<mapping deploy="/" local="$PROJECT_DIR$" web="/" />
</mappings>
</serverdata>
</paths>
<paths name="vpn.it-stories.ru">
<serverdata>
<mappings>
<mapping local="$PROJECT_DIR$" web="/" />
</mappings>
</serverdata>
</paths>
<paths name="vpnservice.it-stories.ru">
<serverdata>
<mappings>
<mapping local="$PROJECT_DIR$" web="/" />
</mappings>
</serverdata>
</paths>
</serverData>
<option name="myAutoUpload" value="ALWAYS" />
</component>
</project>

8
.idea/modules.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/api.rsgrinko.ru.iml" filepath="$PROJECT_DIR$/.idea/api.rsgrinko.ru.iml" />
</modules>
</component>
</project>

20
.idea/php.xml generated Normal file
View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="MessDetectorOptionsConfiguration">
<option name="transferred" value="true" />
</component>
<component name="PHPCSFixerOptionsConfiguration">
<option name="transferred" value="true" />
</component>
<component name="PHPCodeSnifferOptionsConfiguration">
<option name="highlightLevel" value="WARNING" />
<option name="transferred" value="true" />
</component>
<component name="PhpProjectSharedConfiguration" php_language_level="8.2" />
<component name="PhpStanOptionsConfiguration">
<option name="transferred" value="true" />
</component>
<component name="PsalmOptionsConfiguration">
<option name="transferred" value="true" />
</component>
</project>

8
.idea/sshConfigs.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="SshConfigs">
<configs>
<sshConfig authType="PASSWORD" host="10.10.10.54" id="4d9eefaf-dfcd-4d93-aabd-263374f3c231" port="22" nameFormat="DESCRIPTIVE" username="rsgrinko" useOpenSSHConfig="true" />
</configs>
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

14
.idea/webServers.xml generated Normal file
View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="WebServers">
<option name="servers">
<webServer id="2cc1e0a6-f2fe-4fcc-8152-87b3c9c34bd0" name="vm-web-02 (10.10.10.54)" url="http://api.rsgrinko.ru">
<fileTransfer rootFolder="/var/www/api.rsgrinko.ru/public_html" accessType="SFTP" host="10.10.10.54" port="22" sshConfigId="4d9eefaf-dfcd-4d93-aabd-263374f3c231" sshConfig="rsgrinko@10.10.10.54:22 password">
<advancedOptions>
<advancedOptions dataProtectionLevel="Private" keepAliveTimeout="0" passiveMode="true" shareSSLContext="true" />
</advancedOptions>
</fileTransfer>
</webServer>
</option>
</component>
</project>

6
api.php Normal file
View File

@@ -0,0 +1,6 @@
<?php
require_once __DIR__ . '/inc/bootstrap.php';
$voltage = $_REQUEST['voltage'] ?? 0;
file_put_contents(STORAGE_DIR . '/voltageNodeMCU.txt', $voltage);
echo 'ok';

32
cron.php Normal file
View File

@@ -0,0 +1,32 @@
<?php
require_once __DIR__ . '/inc/bootstrap.php';
// Получение данных с Dessmonitor
$anenjBatVoltage = 0;
$anenjPvVoltage = 0;
$anenjPvCurrent = 0;
$anenjTemp = 0;
$dessmonData = @file_get_contents('https://web.dessmonitor.com/public/?sign=6f58bbf6402c97ce614b80876e1cfb5012ca9571&salt=1753542174017&token=3a491f16dfbe8d3e4be66165789fe0536686da0632d6298422992f1bf3af13da&action=querySPDeviceLastData&source=1&devcode=2376&pn=Q0045473081804&devaddr=1&sn=Q0045473081804094801&i18n=en_US');
$dessmonData = json_decode($dessmonData, true);
if (!empty($dessmonData) && $dessmonData['err'] === 0) {
$anenjBatVoltage = $dessmonData['dat']['pars']['bt_'][0]['val'];
$anenjPvVoltage = $dessmonData['dat']['pars']['pv_'][0]['val'];
$anenjPvCurrent = $dessmonData['dat']['pars']['pv_'][1]['val'];
$anenjTempDC = $dessmonData['dat']['pars']['sy_'][1]['val'];
$anenjTempINV = $dessmonData['dat']['pars']['sy_'][2]['val'];
}
$result = [
'anenjBatVoltage' => $anenjBatVoltage,
'anenjPvVoltage' => $anenjPvVoltage,
'anenjPvCurrent' => $anenjPvCurrent,
'anenjTempDC' => $anenjTempDC,
'anenjTempINV' => $anenjTempINV,
];
file_put_contents(__DIR__ . '/data/anenj.txt', serialize($result));
echo 'Task OK';

1
current_voltage.txt Normal file
View File

@@ -0,0 +1 @@
13.52

1
data/anenj.txt Normal file
View File

@@ -0,0 +1 @@
a:5:{s:15:"anenjBatVoltage";s:4:"12.9";s:14:"anenjPvVoltage";s:4:"13.7";s:14:"anenjPvCurrent";s:3:"0.0";s:11:"anenjTempDC";s:2:"33";s:12:"anenjTempINV";s:2:"33";}

1
data/yandex.txt Normal file
View File

@@ -0,0 +1 @@
a:4:{s:5:"state";b:0;s:7:"voltage";d:231.8;s:5:"power";i:0;s:8:"amperage";i:0;}

16
inc/bootstrap.php Normal file
View File

@@ -0,0 +1,16 @@
<?php
const STORAGE_DIR = __DIR__ . '/../data';
const STORAGE_RUN_DIR = __DIR__ . '/../run';
require_once __DIR__ . '/lib/YandexIoT.php';
require_once __DIR__ . '/lib/Prometheus.php';
require_once __DIR__ . '/lib/Narodmon.php';
/** Идентификатор устройства народного мониторинга */
const NARODMON_DEVICE_ID = 'AE-71-70-50-00-01';
// Яндекс IoT
const STORAGE_YANDEX_FILE = STORAGE_DIR . '/yandexTokenData.txt';
const YANDEX_CLIENT_ID = 'ee129ac1d4384bb3b1a316834c2029bc';
const YANDEX_CLIENT_SECRET = 'b2777cb489794901828581184623b22a';

34
inc/lib/Narodmon.php Normal file
View File

@@ -0,0 +1,34 @@
<?php
class Narodmon
{
private const API_ENDPOINT = 'http://narodmon.ru/post';
private string $deviceMac;
private array $data = [];
public function __construct(string $deviceMac)
{
$this->deviceMac = $deviceMac;
}
public function add(string $name, int|float $value): self
{
$this->data[$name] = $value;
return $this;
}
public function send()
{
$data = array_merge($this->data, ['ID' => $this->deviceMac]);
$ch = curl_init(self::API_ENDPOINT);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 5);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
$reply = curl_exec($ch);
curl_close($ch);
}
}

30
inc/lib/Prometheus.php Normal file
View File

@@ -0,0 +1,30 @@
<?php
class Prometheus
{
private array $data = [];
public function __construct()
{
}
public function add(string $name, string|int|float $value): self
{
$this->data[$name] = $value;
return $this;
}
public function getData(): string
{
$result = '';
foreach ($this->data as $key => $value) {
$result .= $key . ': ' . $value . PHP_EOL;
}
return $result;
}
public function showData(): void
{
echo $this->getData();
}
}

199
inc/lib/YandexIoT.php Normal file
View File

@@ -0,0 +1,199 @@
<?php
class YandexIoT
{
private string $clientId;
private string $clientSecret;
private ?string $accessToken = null;
private ?string $refreshToken = null;
public function __construct(
string $clientId,
string $clientSecret,
?string $accessToken = null,
?string $refreshToken = null
) {
$this->clientId = $clientId;
$this->clientSecret = $clientSecret;
$this->accessToken = $accessToken;
$this->refreshToken = $refreshToken;
}
/**
* @throws JsonException
*/
public function initFromFile(string $file): self
{
if (!file_exists($file)) {
throw new RuntimeException('Файл для инициализации не найден');
}
$data = json_decode(file_get_contents($file), true, 512, JSON_THROW_ON_ERROR);
$this->accessToken = $data['access_token'] ?? null;
$this->refreshToken = $data['access_token'] ?? null;
return $this;
}
/**
* Отправка запроса
*
* @param string $url
* @param array $headers
* @param array|null $data
* @param string|null $method
* @return array
* @throws JsonException
*/
public static function sendRequest(string $url, array $headers = [], ?array $data = null, ?string $method = null): array
{
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
if (!empty($method)) {
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
}
if (!empty($data)) {
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
}
curl_setopt($ch, CURLOPT_FAILONERROR, 0);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 0);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_VERBOSE, 0);
curl_setopt($ch, CURLOPT_TIMEOUT, 5);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
$result = curl_exec($ch);
$result = json_decode($result, true, 512, JSON_THROW_ON_ERROR);
curl_close($ch);
return $result;
}
/**
* @throws JsonException
*/
public function sendYandexRequest(
string $function,
array $additionalHeaders = [],
?array $data = null,
?string $method = null
): array {
$apiUrl = 'https://api.iot.yandex.net';
$url = $apiUrl . $function;
$headers = array_merge(['Authorization: Bearer ' . $this->getAccessToken()], $additionalHeaders);
return (self::sendRequest($url, $headers, $data, $method));
}
/**
* @throws JsonException
*/
public function getPowerState(string $deviceId): bool
{
$function = '/v1.0/devices/' . $deviceId;
$result = $this->sendYandexRequest($function);
if (isset($result['capabilities'])) {
foreach ($result['capabilities'] as $capability) {
if ($capability['type'] === 'devices.capabilities.on_off') {
return (int)$capability['state']['value'] === 1;
}
}
}
return false;
}
/**
* @throws JsonException
*/
public function getProps(string $deviceId): array
{
$res = [];
$function = '/v1.0/devices/' . $deviceId;
$result = $this->sendYandexRequest($function);
// состояние по умолчанию
$res['state'] = false;
if (isset($result['capabilities'])) {
foreach ($result['capabilities'] as $capability) {
if ($capability['type'] === 'devices.capabilities.on_off' && (int)$capability['state']['value'] === 1) {
$res['state'] = true;
break;
}
}
}
if (isset($result['properties'])) {
foreach ($result['properties'] as $prop) {
$res[$prop['state']['instance']] = $prop['state']['value'];
}
}
return $res;
}
/**
* @throws JsonException
*/
public function getInfo(): array
{
$function = '/v1.0/user/info';
return $this->sendYandexRequest($function);
}
/**
* @throws JsonException
*/
public function setPowerState(string $deviceId, bool $state): bool
{
$function = '/v1.0/devices/actions';
$headers = ['Content-Type: application/json'];
$data = [
'devices' => [
'actions' => [
'state' => [
'instance' => 'on',
'value' => $state ? 'true' : 'false',
],
'type' => 'devices.capabilities.on_off',
],
'id' => $deviceId,
],
];
$result = $this->sendYandexRequest($function, $headers, $data);
return isset($result['status']) && $result['status'] === 'ok';
}
public function getAccessToken(): ?string
{
return $this->accessToken;
}
public function getRefreshToken(): ?string
{
return $this->refreshToken;
}
/**
* @throws JsonException
*/
public function updateToken(): bool
{
$url = 'https://oauth.yandex.ru/token';
$auth = base64_encode($this->clientId . ':' . $this->clientSecret);
$headers = [
'Authorization: Basic ' . $auth,
'application/x-www-form-urlencoded'
];
$data = [
'grant_type' => 'refresh_token',
'refresh_token' => $this->getRefreshToken(),
];
$response = self::sendRequest($url, $headers, $data);
if (isset($response['access_token'])) {
$this->accessToken = '';
$this->refreshToken = '';
return true;
}
return false;
}
}

50
index.php Normal file
View File

@@ -0,0 +1,50 @@
<html>
<head>
<title>API Server | <?= gethostname() ?></title>
<style>
.box {
width: 300px;
margin: 0 auto;
background: #f7f7f7;
border: 1px solid #ededed;
padding: 10px;
font-size: 2em;
margin-top: 25px;
text-align: center;
color: #636363;
font-family: monospace;
}
.footer {
margin: 0 auto;
font-size: 1em;
margin-top: 25px;
text-align: left;
color: #636363;
font-family: monospace;
width: 400px;
color: gray;
}
.bold {
font-weight: bold;
color: black;
}
.uname {
margin: 0 auto;
margin-top: 25px;
text-align: center;
color: #636363;
font-family: monospace;
}
</style>
</head>
<body>
<div class="box">External API server</div>
<div class="footer">
<span class="bold">Hostname:</span> <?= shell_exec('hostname') ?><br>
<span class="bold">Internal IP:</span> <?= shell_exec('ifconfig ens18 | grep \'inet \'| grep -v \'127.0.0.1\' | cut -d: -f2 | awk \'{ print $2}\'') ?><br>
<span class="bold">Document root:</span> <?php echo $_SERVER['DOCUMENT_ROOT']; ?><br>
<span class="bold">Disk usage:</span> <?= shell_exec('df -h | grep sda') ?><br>
</div>
<div class="uname"><?= shell_exec('uname -a') ?></div>
</body>
</html>

23
iot/iot_yandex_auth.php Normal file
View File

@@ -0,0 +1,23 @@
<?php
require_once __DIR__ . '/../inc/bootstrap.php';
if (isset($_GET['code'])) {
$auth = base64_encode(YANDEX_CLIENT_ID . ':' . YANDEX_CLIENT_SECRET);
$headers = ['Authorization: Basic ' . $auth];
$url = 'https://oauth.yandex.ru/token';
$data = [
'grant_type' => 'authorization_code',
'code' => $_GET['code'],
];
$response = YandexIoT::sendRequest($url, $headers, $data);
if (isset($response['access_token'])) {
$response['expires'] = $response['expires_in'] + time();
if (file_put_contents(STORAGE_YANDEX_FILE, json_encode($response, JSON_THROW_ON_ERROR))) {
echo 'Success';
}
die();
}
}
header('Location: https://oauth.yandex.ru/authorize?response_type=code&client_id=' . YANDEX_CLIENT_ID . '&force_confirm=yes&scope=iot:view%20iot:control');

1
narodmonFile.txt Normal file
View File

@@ -0,0 +1 @@
1753965307

52
prometheus.php Normal file
View File

@@ -0,0 +1,52 @@
<?php
require_once __DIR__ . '/inc/bootstrap.php';
header('Content-Type: text/plain');
$prometheusObject = new Prometheus();
$dataFile = STORAGE_DIR . '/voltageNodeMCU.txt';
$voltage = 0;
if (file_exists($dataFile) && (time() - filectime($dataFile) < 60)) {
$voltage = file_get_contents($dataFile);
}
$prometheusObject->add('voltage', $voltage);
/** Получение данных с Dessmonitor */
$anenjiData = unserialize(file_get_contents(__DIR__ . '/data/anenj.txt'));
$prometheusObject->add('anenj_bat_voltage', $anenjiData['anenjBatVoltage'] ?? 0)
->add('anenj_pv_voltage', $anenjiData['anenjPvVoltage'] ?? 0)
->add('anenj_pv_current', $anenjiData['anenjPvCurrent'] ?? 0)
->add('anenj_temp_dc', $anenjiData['anenjTempDC'] ?? 0)
->add('anenj_temp_inv', $anenjiData['anenjTempINV'] ?? 0);
/** Получение данных с Yandex IoT */
$yandexObject = (new YandexIoT(YANDEX_CLIENT_ID, YANDEX_CLIENT_SECRET))->initFromFile(STORAGE_YANDEX_FILE);
$yandexData = $yandexObject->getProps('c42a0011-71cc-4d6c-bef9-93833b2ae67f');
$prometheusObject->add('yandex_home_voltage', $yandexData['voltage'] ?? 0);
/** Отправка данных в народный мониторинг */
$flagNarodFile = STORAGE_RUN_DIR . '/narodmon.run';
if (!file_exists($flagNarodFile)) {
file_put_contents($flagNarodFile, time());
}
$sendToNarodMon = false;
if ($voltage !== 0 && (time() - filectime($flagNarodFile) > 60)) {
file_put_contents($flagNarodFile, time());
$sendToNarodMon = true;
(new Narodmon(NARODMON_DEVICE_ID))->add('V1', $voltage)
->add('U2', $anenjData['anenjBatVoltage'] ?? 0)
->add('U3', $anenjData['anenjPvVoltage'] ?? 0)
->add('T2', $anenjData['anenjTempDC'] ?? 0)
->add('T3', $anenjData['anenjTempINV'] ?? 0)
->add('I1', $anenjData['anenjPvCurrent'] ?? 0)
->send();
}
$prometheusObject->add('narodmon', $sendToNarodMon ? 1 : 0);
$prometheusObject->showData();

2204596
voltage.txt Normal file

File diff suppressed because it is too large Load Diff