├── .releaseconfig.json ├── admin ├── netatmo.png ├── i18n │ ├── zh-cn │ │ └── translations.json │ ├── en │ │ └── translations.json │ ├── nl │ │ └── translations.json │ ├── pt │ │ └── translations.json │ ├── pl │ │ └── translations.json │ ├── es │ │ └── translations.json │ ├── it │ │ └── translations.json │ ├── uk │ │ └── translations.json │ ├── ru │ │ └── translations.json │ ├── de │ │ └── translations.json │ └── fr │ │ └── translations.json ├── jsonConfig.json └── words.js ├── .gitignore ├── test ├── mocha.custom.opts ├── package.js ├── tsconfig.json ├── unit.js ├── integration.js └── mocha.setup.js ├── .github ├── dependabot.yml ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── auto-merge.yml ├── workflows │ ├── dependabot-automerge.yml │ ├── codeql.yml │ └── test-and-release.yml └── stale.yml ├── LICENSE ├── package.json ├── lib ├── eventEmitterBridge.js ├── netatmoBubendorff.js ├── netatmoCOSensor.js ├── netatmoSmokedetector.js └── netatmoCoach.js ├── README.md ├── main.js └── io-package.json /.releaseconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["iobroker", "license"] 3 | } 4 | -------------------------------------------------------------------------------- /admin/netatmo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PArns/ioBroker.netatmo/HEAD/admin/netatmo.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | .DS_Store 3 | .idea/ 4 | package-lock.json 5 | 6 | # ioBroker dev-server 7 | .dev-server/ 8 | -------------------------------------------------------------------------------- /test/mocha.custom.opts: -------------------------------------------------------------------------------- 1 | --require test/mocha.setup.js 2 | {!(node_modules|test)/**/*.test.js,*.test.js,test/**/test!(PackageFiles|Startup).js} -------------------------------------------------------------------------------- /test/package.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const { tests } = require('@iobroker/testing'); 3 | 4 | // Validate the package files 5 | tests.packageFiles(path.join(__dirname, '..')); 6 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "noImplicitAny": false 5 | }, 6 | "include": [ 7 | "./**/*.js" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /test/unit.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const { tests } = require('@iobroker/testing'); 3 | 4 | // Run unit tests - See https://github.com/ioBroker/testing for a detailed explanation and further options 5 | tests.unit(path.join(__dirname, '..')); 6 | -------------------------------------------------------------------------------- /test/integration.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const { tests } = require('@iobroker/testing'); 3 | 4 | // Run integration tests - See https://github.com/ioBroker/testing for a detailed explanation and further options 5 | tests.integration(path.join(__dirname, '..')); 6 | -------------------------------------------------------------------------------- /test/mocha.setup.js: -------------------------------------------------------------------------------- 1 | // Don't silently swallow unhandled rejections 2 | process.on('unhandledRejection', (e) => { 3 | throw e; 4 | }); 5 | 6 | // enable the should interface with sinon 7 | // and load chai-as-promised and sinon-chai by default 8 | const sinonChai = require('sinon-chai'); 9 | const chaiAsPromised = require('chai-as-promised'); 10 | const { should, use } = require('chai'); 11 | 12 | should(); 13 | use(sinonChai); 14 | use(chaiAsPromised); -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # Maintain dependencies for GitHub Actions 4 | - package-ecosystem: github-actions 5 | directory: "/" 6 | schedule: 7 | interval: monthly 8 | time: "04:00" 9 | timezone: Europe/Berlin 10 | - package-ecosystem: npm 11 | directory: "/" 12 | schedule: 13 | interval: monthly 14 | time: "04:00" 15 | timezone: Europe/Berlin 16 | open-pull-requests-limit: 20 17 | versioning-strategy: increase -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/auto-merge.yml: -------------------------------------------------------------------------------- 1 | # Configure here which dependency updates should be merged automatically. 2 | # The recommended configuration is the following: 3 | - match: 4 | # Only merge patches for production dependencies 5 | dependency_type: production 6 | update_type: "semver:patch" 7 | - match: 8 | # Except for security fixes, here we allow minor patches 9 | dependency_type: production 10 | update_type: "security:minor" 11 | - match: 12 | # and development dependencies can have a minor update, too 13 | dependency_type: development 14 | update_type: "semver:minor" 15 | 16 | # The syntax is based on the legacy dependabot v1 automerged_updates syntax, see: 17 | # https://dependabot.com/docs/config-file/#automerged_updates -------------------------------------------------------------------------------- /.github/workflows/dependabot-automerge.yml: -------------------------------------------------------------------------------- 1 | # Automatically merge Dependabot PRs when version comparison is within the range 2 | # that is configured in .github/auto-merge.yml 3 | 4 | name: Auto-Merge Dependabot PRs 5 | 6 | on: 7 | pull_request_target: 8 | 9 | jobs: 10 | auto-merge: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@v3 15 | 16 | - name: Check if PR should be auto-merged 17 | uses: ahmadnassri/action-dependabot-auto-merge@v2 18 | with: 19 | # This must be a personal access token with push access 20 | github-token: ${{ secrets.AUTO_MERGE_TOKEN }} 21 | # By default, squash and merge, so Github chooses nice commit messages 22 | command: squash and merge -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Something is not working as it should 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **To Reproduce** 13 | Steps to reproduce the behavior: 14 | 1. Go to '...' 15 | 2. Click on '...' 16 | 3. Scroll down to '....' 17 | 4. See error 18 | 19 | **Expected behavior** 20 | A clear and concise description of what you expected to happen. 21 | 22 | **Screenshots & Logfiles** 23 | If applicable, add screenshots and logfiles to help explain your problem. 24 | 25 | **Versions:** 26 | - Adapter version: 27 | - JS-Controller version: 28 | - Node version: 29 | - Operating system: 30 | 31 | **Additional context** 32 | Add any other context about the problem here. 33 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | # The branches below must be a subset of the branches above 8 | branches: [ "master" ] 9 | schedule: 10 | - cron: '31 17 * * 1' 11 | 12 | jobs: 13 | analyze: 14 | name: Analyze 15 | runs-on: ubuntu-latest 16 | permissions: 17 | actions: read 18 | contents: read 19 | security-events: write 20 | 21 | strategy: 22 | fail-fast: false 23 | matrix: 24 | language: [ 'javascript' ] 25 | 26 | steps: 27 | - name: Checkout repository 28 | uses: actions/checkout@v3 29 | 30 | # Initializes the CodeQL tools for scanning. 31 | - name: Initialize CodeQL 32 | uses: github/codeql-action/init@v2 33 | with: 34 | languages: ${{ matrix.language }} 35 | 36 | - name: Autobuild 37 | uses: github/codeql-action/autobuild@v2 38 | 39 | - name: Perform CodeQL Analysis 40 | uses: github/codeql-action/analyze@v2 41 | with: 42 | category: "/language:${{matrix.language}}" 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016-2025 Patrick Arns 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /admin/i18n/zh-cn/translations.json: -------------------------------------------------------------------------------- 1 | { 2 | "Authentication information": "请将上述但有礼拜通知你的内联网mo帐户,以便获取数据。 你们如果你在你的账户中增加更多的装置型,你就可能错失数据!", 3 | "CO": "一氧化碳传感器", 4 | "CheckIntervall": "数据更新间隔", 5 | "CleanupIntervall": "数据清理间隔", 6 | "ClientID": "个人客户 ID", 7 | "ClientSecret": "个人客户秘密", 8 | "E-Mail": "电子邮件", 9 | "Elevation": "海拔", 10 | "Health Coach": "健康教练", 11 | "Login with Netatmo": "D. 净摩的物质", 12 | "Netatmo App": "从您的 Netatmo 应用程序", 13 | "Password": "密码", 14 | "RemoveEvents": "删除早于的事件", 15 | "RemoveUnknownPerson": "删除最后一次见到的不知名人士", 16 | "Smokedetector": "烟雾探测器", 17 | "Weather station": "气象站", 18 | "Welcome indoor cam": "世界自然协会", 19 | "_additionalSettingsHeader": "其他设置", 20 | "_realtimeEventHeader": "实时事件", 21 | "_realtimeEventInfo": "为了从 Welcome & Presence 接收实时事件或获得实时烟雾警报、门铃或 CO 传感器,您需要选择一个具有活动助手或远程许可证的物联网适配器实例来传递事件数据!", 22 | "auth_info_individual_credentials": "输入或更改专用客户端 ID 后,请使用上述按钮重复验证过程。", 23 | "clean minutes": "(分钟)", 24 | "hours": "(小时)", 25 | "iotInstanceLabel": "实时事件的物联网实例", 26 | "live_stream1": "为了增加更新间隔或接收来自 Welcome &存在或获取实时烟雾警报、门铃或 CO 传感器,您必须向 Netatmo 请求您自己的 API 密钥!", 27 | "live_stream2": "为此,请访问以下 URL,使用您的 Netatmo 帐户登录并填写请求的表格", 28 | "load minutes": "(分钟)", 29 | "meters": "米超过 nN", 30 | "netatmoBubendorff": "iDiamant/布本多夫" 31 | } 32 | -------------------------------------------------------------------------------- /admin/i18n/en/translations.json: -------------------------------------------------------------------------------- 1 | { 2 | "Authentication information": "Please press the above button to open a window to authenticate with your Netatmo Account to allow to access the data. You need to authenticate again whenever you add more device-types to your account, else you might miss data! if no window opens please check your Popup blocker settings!", 3 | "CO": "CO sensor", 4 | "CheckIntervall": "Data update interval", 5 | "CleanupIntervall": "Data cleanup interval", 6 | "ClientID": "Individual Client ID", 7 | "ClientSecret": "Individual Client secret", 8 | "E-Mail": "E-Mail", 9 | "Elevation": "Elevation", 10 | "Health Coach": "Healthy Home Coach", 11 | "Login with Netatmo": "Login with Netatmo", 12 | "Netatmo App": "From your Netatmo App", 13 | "Password": "Password", 14 | "RemoveEvents": "Remove events older than", 15 | "RemoveUnknownPerson": "Remove unknown persons last seen older than", 16 | "Smokedetector": "smoke detector", 17 | "Weather station": "Weather station", 18 | "Welcome indoor cam": "Welcome indoor/outdoor camera", 19 | "_additionalSettingsHeader": "Additional Settings", 20 | "_realtimeEventHeader": "Realtime Events", 21 | "_realtimeEventInfo": "In order to receive realtime events from Welcome & Presence or to get realtime smoke alerts, doorbell or the CO sensor, you need to select an iot Adapter instance that has an active Assistant or Remote License to pass through the event data!", 22 | "auth_info_individual_credentials": "After entering or changing an individual client-ID please repeat the Authentication process using the above button.", 23 | "clean minutes": "(minutes)", 24 | "hours": "(hours)", 25 | "iotInstanceLabel": "iot Instance for Realtime Events", 26 | "live_stream1": "In order to increase update interval or receive livestreams from Welcome & Presence or to get realtime smoke alerts, doorbell or the CO sensor, you've to request your own API key from Netatmo!", 27 | "live_stream2": "To do so, go to the following URL, login with your Netatmo account and fill out the requested form", 28 | "load minutes": "(minutes)", 29 | "meters": "meters over nN", 30 | "netatmoBubendorff": "iDiamant/Bubendorff" 31 | } 32 | -------------------------------------------------------------------------------- /admin/i18n/nl/translations.json: -------------------------------------------------------------------------------- 1 | { 2 | "Authentication information": "Druk op de bovenste knop om te authenticeren met je Netatmo Account om toegang te krijgen tot de gegevens. Je moet opnieuw authenticeren wanneer je meer apparaat-types op je rekening voegt, anders mis je data!", 3 | "CO": "CO-sensor", 4 | "CheckIntervall": "Interval voor gegevensupdate", 5 | "CleanupIntervall": "Interval voor het opschonen van gegevens", 6 | "ClientID": "Individuele klant-ID", 7 | "ClientSecret": "Individueel klantgeheim", 8 | "E-Mail": "E-mail", 9 | "Elevation": "Verhoging", 10 | "Health Coach": "Gezondheidscoach", 11 | "Login with Netatmo": "Login met Netatmo", 12 | "Netatmo App": "Vanuit uw Netatmo-app", 13 | "Password": "Wachtwoord", 14 | "RemoveEvents": "Gebeurtenissen ouder dan . verwijderen", 15 | "RemoveUnknownPerson": "Verwijder onbekende personen die het laatst gezien ouder zijn dan", 16 | "Smokedetector": "rookdetector", 17 | "Weather station": "Weerstation", 18 | "Welcome indoor cam": "Welcome binnencamera", 19 | "_additionalSettingsHeader": "Aanvullende instellingen", 20 | "_realtimeEventHeader": "Realtime evenementen", 21 | "_realtimeEventInfo": "Om realtime gebeurtenissen van Welcome & Presence te ontvangen of om realtime rookmeldingen, deurbel of de CO-sensor te ontvangen, moet u een iot Adapter-instantie selecteren die een actieve assistent of licentie op afstand heeft om de gebeurtenisgegevens door te geven!", 22 | "auth_info_individual_credentials": "Na het invoeren of wijzigen van een toegewezen client-ID, herhaalt u het authenticatieproces met behulp van de bovenstaande knop.", 23 | "clean minutes": "(minuten)", 24 | "hours": "(uur)", 25 | "iotInstanceLabel": "iot-instantie voor realtime gebeurtenissen", 26 | "live_stream1": "Om het update-interval te verlengen of livestreams te ontvangen van Welcome & Aanwezigheid of om realtime rookwaarschuwingen, deurbel of de CO-sensor te krijgen, moet u uw eigen API-sleutel bij Netatmo aanvragen!", 27 | "live_stream2": "Ga hiervoor naar de volgende URL, log in met uw Netatmo-account en vul het gevraagde formulier in", 28 | "load minutes": "(minuten)", 29 | "meters": "meter boven nN", 30 | "netatmoBubendorff": "iDiamant/Bubendorff" 31 | } 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "iobroker.netatmo", 3 | "version": "3.1.0", 4 | "description": "ioBroker netatmo Adapter", 5 | "author": "Patrick Arns ", 6 | "contributors": [ 7 | { 8 | "name": "Patrick Arns", 9 | "email": "iobroker@patrick-arns.de" 10 | }, 11 | { 12 | "name": "Peter Weiss", 13 | "email": "peter.weiss@wep4you.com" 14 | }, 15 | { 16 | "name": "Dom", 17 | "email": "dom@bugger.ch" 18 | } 19 | ], 20 | "homepage": "https://github.com/PArns/ioBroker.netatmo/", 21 | "license": "MIT", 22 | "keywords": [ 23 | "ioBroker", 24 | "netatmo", 25 | "welcome", 26 | "camera", 27 | "Smart Home", 28 | "home automation" 29 | ], 30 | "repository": { 31 | "type": "git", 32 | "url": "https://github.com/PArns/ioBroker.netatmo" 33 | }, 34 | "dependencies": { 35 | "@iobroker/adapter-core": "^3.2.3", 36 | "dewpoint": "^1.0.0", 37 | "moment": "^2.30.1", 38 | "request": "^2.88.2" 39 | }, 40 | "main": "main.js", 41 | "files": [ 42 | "admin/", 43 | "lib/", 44 | "io-package.json", 45 | "main.js", 46 | "LICENSE" 47 | ], 48 | "devDependencies": { 49 | "@alcalzone/release-script": "^3.8.0", 50 | "@alcalzone/release-script-plugin-iobroker": "^3.7.2", 51 | "@alcalzone/release-script-plugin-license": "^3.7.0", 52 | "@iobroker/adapter-dev": "^1.4.0", 53 | "@iobroker/testing": "^5.0.4", 54 | "@iobroker/dev-server": "^0.7.8", 55 | "mocha": "^11.5.0", 56 | "chai": "^4.5.0" 57 | }, 58 | "bugs": { 59 | "url": "https://github.com/ioBroker/ioBroker.netatmo/issues" 60 | }, 61 | "readmeFilename": "README.md", 62 | "scripts": { 63 | "test:js": "mocha --opts test/mocha.custom.opts", 64 | "test:package": "mocha test/package --exit", 65 | "test:unit": "mocha test/unit --exit", 66 | "test:integration": "mocha test/integration --exit", 67 | "test": "npm run test:js && npm run test:package", 68 | "release": "release-script", 69 | "release-patch": "release-script patch --yes", 70 | "release-minor": "release-script minor --yes", 71 | "release-major": "release-script major --yes", 72 | "translate": "translate-adapter", 73 | "dev-server": "dev-server" 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /admin/i18n/pt/translations.json: -------------------------------------------------------------------------------- 1 | { 2 | "Authentication information": "Por favor, pressione o botão acima para autenticar com sua Conta Netatmo para permitir acessar os dados. Você precisa autenticar novamente sempre que adicionar mais tipos de dispositivo à sua conta, então você pode perder dados!", 3 | "CO": "sensor de CO", 4 | "CheckIntervall": "Intervalo de atualização de dados", 5 | "CleanupIntervall": "Intervalo de limpeza de dados", 6 | "ClientID": "ID de cliente individual", 7 | "ClientSecret": "Segredo do cliente individual", 8 | "E-Mail": "O email", 9 | "Elevation": "Elevação", 10 | "Health Coach": "Coach de Saúde", 11 | "Login with Netatmo": "Login com Netatmo", 12 | "Netatmo App": "Do seu aplicativo Netatmo", 13 | "Password": "Senha", 14 | "RemoveEvents": "Remover eventos anteriores a", 15 | "RemoveUnknownPerson": "Remover pessoas desconhecidas vistas pela última vez com mais de", 16 | "Smokedetector": "detector de fumaça", 17 | "Weather station": "Estação meteorológica", 18 | "Welcome indoor cam": "Welcome câmera interna/exterior", 19 | "_additionalSettingsHeader": "Configurações adicionais", 20 | "_realtimeEventHeader": "Eventos em tempo real", 21 | "_realtimeEventInfo": "Para receber eventos em tempo real de boas-vindas e presença ou para obter alertas de fumaça, campainha ou sensor de CO em tempo real, você precisa selecionar uma instância do adaptador iot que tenha um assistente ativo ou licença remota para passar os dados do evento!", 22 | "auth_info_individual_credentials": "Depois de inserir ou alterar um ID de cliente dedicado, repita o processo de autenticação usando o botão acima.", 23 | "clean minutes": "(minutos)", 24 | "hours": "(horas)", 25 | "iotInstanceLabel": "instância iot para eventos em tempo real", 26 | "live_stream1": "Para aumentar o intervalo de atualização ou receber transmissões ao vivo de Welcome & Presença ou para obter alertas de fumaça em tempo real, campainha ou sensor de CO, você deve solicitar sua própria chave de API da Netatmo!", 27 | "live_stream2": "Para isso, acesse a seguinte URL, faça login com sua conta Netatmo e preencha o formulário solicitado", 28 | "load minutes": "(minutos)", 29 | "meters": "metros sobre nN", 30 | "netatmoBubendorff": "iDiamant/Bubendorff" 31 | } 32 | -------------------------------------------------------------------------------- /admin/i18n/pl/translations.json: -------------------------------------------------------------------------------- 1 | { 2 | "Authentication information": "Proszę nad przyciskiem, aby uwierzyć w twoją Netatmo Account, aby umożliwić dostęp do danych. Znów potrzebujesz uwierzytelniania, kiedy dodasz więcej typów urządzenie do twojego konta, inni moglibyś znieść dane!", 3 | "CO": "Czujnik CO", 4 | "CheckIntervall": "Interwał aktualizacji danych", 5 | "CleanupIntervall": "Interwał czyszczenia danych", 6 | "ClientID": "Indywidualny identyfikator klienta", 7 | "ClientSecret": "Indywidualny sekret Klienta", 8 | "E-Mail": "E-mail", 9 | "Elevation": "Podniesienie", 10 | "Health Coach": "Trener zdrowia", 11 | "Login with Netatmo": "Login", 12 | "Netatmo App": "Z Twojej aplikacji Netatmo", 13 | "Password": "Hasło", 14 | "RemoveEvents": "Usuń wydarzenia starsze niż", 15 | "RemoveUnknownPerson": "Usuń nieznane osoby, które ostatnio widziano starsze niż", 16 | "Smokedetector": "Czujnik dymu", 17 | "Weather station": "Stacja pogodowa", 18 | "Welcome indoor cam": "Welcome kamera wewnętrzna", 19 | "_additionalSettingsHeader": "Dodatkowe ustawienia", 20 | "_realtimeEventHeader": "Zdarzenia w czasie rzeczywistym", 21 | "_realtimeEventInfo": "Aby otrzymywać zdarzenia w czasie rzeczywistym z Welcome & Presence lub otrzymywać powiadomienia o dymie, dzwonek do drzwi lub czujnik CO w czasie rzeczywistym, musisz wybrać instancję adaptera iot, która ma aktywnego asystenta lub licencję zdalną, aby przekazywać dane zdarzeń!", 22 | "auth_info_individual_credentials": "Po wprowadzeniu lub zmianie dedykowanego identyfikatora klienta należy powtórzyć proces Uwierzytelniania za pomocą powyższego przycisku.", 23 | "clean minutes": "(minuty)", 24 | "hours": "(godziny)", 25 | "iotInstanceLabel": "Instancja iot dla zdarzeń w czasie rzeczywistym", 26 | "live_stream1": "Aby wydłużyć interwał aktualizacji lub odbierać transmisje na żywo z Welcome & Obecność lub aby otrzymywać powiadomienia o dymie w czasie rzeczywistym, dzwonek do drzwi lub czujnik CO, musisz poprosić Netatmo o własny klucz API!", 27 | "live_stream2": "Aby to zrobić, przejdź pod następujący adres URL, zaloguj się na swoje konto Netatmo i wypełnij żądany formularz", 28 | "load minutes": "(minuty)", 29 | "meters": "metry ponad nN", 30 | "netatmoBubendorff": "iDiamant/Bubendorff" 31 | } 32 | -------------------------------------------------------------------------------- /admin/i18n/es/translations.json: -------------------------------------------------------------------------------- 1 | { 2 | "Authentication information": "Por favor, presione el botón anterior para autenticar con su cuenta Netatmo para permitir el acceso a los datos. Necesitas autenticar de nuevo cada vez que agregas más tipos de dispositivo a tu cuenta, ¡de lo contrario podrías perder datos!", 3 | "CO": "sensor de monóxido de carbono", 4 | "CheckIntervall": "Intervalo de actualización de datos", 5 | "CleanupIntervall": "Intervalo de limpieza de datos", 6 | "ClientID": "ID de cliente individual", 7 | "ClientSecret": "Secreto de cliente individual", 8 | "E-Mail": "Email", 9 | "Elevation": "Elevación", 10 | "Health Coach": "entrenador de salud", 11 | "Login with Netatmo": "Iniciar sesión con Netatmo", 12 | "Netatmo App": "Desde tu App Netatmo", 13 | "Password": "Clave", 14 | "RemoveEvents": "Eliminar eventos anteriores a", 15 | "RemoveUnknownPerson": "Eliminar personas desconocidas vistas por última vez mayores de", 16 | "Smokedetector": "detector de humo", 17 | "Weather station": "Estación meteorológica", 18 | "Welcome indoor cam": "Welcome Cámara interior/exterior", 19 | "_additionalSettingsHeader": "Ajustes adicionales", 20 | "_realtimeEventHeader": "Eventos en tiempo real", 21 | "_realtimeEventInfo": "Para recibir eventos en tiempo real de Bienvenida y presencia o recibir alertas de humo, timbre o sensor de CO en tiempo real, debe seleccionar una instancia de adaptador iot que tenga un asistente activo o una licencia remota para pasar los datos del evento.", 22 | "auth_info_individual_credentials": "Después de ingresar o cambiar una identificación de cliente dedicada, repita el proceso de autenticación usando el botón anterior.", 23 | "clean minutes": "(minutos)", 24 | "hours": "(horas)", 25 | "iotInstanceLabel": "Instancia iot para eventos en tiempo real", 26 | "live_stream1": "Para aumentar el intervalo de actualización o recibir transmisiones en vivo de Welcome & Presencia o para recibir alertas de humo en tiempo real, timbre o el sensor de CO, ¡debe solicitar su propia clave API de Netatmo!", 27 | "live_stream2": "Para ello, acceda a la siguiente URL, inicie sesión con su cuenta Netatmo y rellene el formulario solicitado", 28 | "load minutes": "(minutos)", 29 | "meters": "metros sobre nN", 30 | "netatmoBubendorff": "iDiamant/Bubendorff" 31 | } 32 | -------------------------------------------------------------------------------- /admin/i18n/it/translations.json: -------------------------------------------------------------------------------- 1 | { 2 | "Authentication information": "Si prega di premere il pulsante sopra per autenticare con il proprio account Netatmo per consentire di accedere ai dati. È necessario autenticare di nuovo ogni volta che si aggiungono più tipi di dispositivo al tuo account, altrimenti si potrebbero perdere i dati!", 3 | "CO": "Sensore di CO", 4 | "CheckIntervall": "Intervallo di aggiornamento dei dati", 5 | "CleanupIntervall": "Intervallo di pulizia dei dati", 6 | "ClientID": "ID cliente individuale", 7 | "ClientSecret": "Segreto del singolo cliente", 8 | "E-Mail": "E-mail", 9 | "Elevation": "Elevazione", 10 | "Health Coach": "Allenatore della salute", 11 | "Login with Netatmo": "Accedi con Netatmo", 12 | "Netatmo App": "Dalla tua App Netatmo", 13 | "Password": "Parola d'ordine", 14 | "RemoveEvents": "Rimuovi gli eventi più vecchi di", 15 | "RemoveUnknownPerson": "Rimuovi le persone sconosciute viste l'ultima volta più vecchie di", 16 | "Smokedetector": "rilevatore di fumo", 17 | "Weather station": "Stazione metereologica", 18 | "Welcome indoor cam": "Welcome telecamera per interni/esterna", 19 | "_additionalSettingsHeader": "Altre impostazioni", 20 | "_realtimeEventHeader": "Eventi in tempo reale", 21 | "_realtimeEventInfo": "Per ricevere eventi in tempo reale da Welcome & Presence o per ricevere avvisi di fumo in tempo reale, campanello o sensore di CO, è necessario selezionare un'istanza dell'adattatore iot con un assistente attivo o una licenza remota per passare attraverso i dati dell'evento!", 22 | "auth_info_individual_credentials": "Dopo aver inserito o modificato un ID cliente dedicato, ripetere il processo di autenticazione utilizzando il pulsante sopra.", 23 | "clean minutes": "(minuti)", 24 | "hours": "(ore)", 25 | "iotInstanceLabel": "Istanza iot per eventi in tempo reale", 26 | "live_stream1": "Per aumentare l'intervallo di aggiornamento o ricevere live streaming da Welcome & Presenza o per ricevere avvisi di fumo in tempo reale, campanello o sensore di CO, devi richiedere la tua chiave API a Netatmo!", 27 | "live_stream2": "Per farlo, vai al seguente URL, accedi con il tuo account Netatmo e compila il modulo richiesto", 28 | "load minutes": "(minuti)", 29 | "meters": "metri su nN", 30 | "netatmoBubendorff": "iDiamant/Bubendorff" 31 | } 32 | -------------------------------------------------------------------------------- /admin/i18n/uk/translations.json: -------------------------------------------------------------------------------- 1 | { 2 | "Authentication information": "Будь ласка, натисніть кнопку вище, щоб автентифікуватися за допомогою свого облікового запису Netatmo, щоб отримати доступ до даних. Щоразу, коли ви додаєте нові типи пристроїв до свого облікового запису, вам потрібно знову проходити автентифікацію, інакше ви можете втратити дані!", 3 | "CO": "датчик CO", 4 | "CheckIntervall": "Інтервал оновлення даних", 5 | "CleanupIntervall": "Інтервал очищення даних", 6 | "ClientID": "Індивідуальний ідентифікатор клієнта", 7 | "ClientSecret": "Індивідуальний секрет клієнта", 8 | "E-Mail": "Електронна пошта", 9 | "Elevation": "Висота", 10 | "Health Coach": "Тренер здоров'я", 11 | "Login with Netatmo": "Увійдіть за допомогою Netatmo", 12 | "Netatmo App": "З вашого додатку Netatmo", 13 | "Password": "Пароль", 14 | "RemoveEvents": "Видалити події, старші ніж", 15 | "RemoveUnknownPerson": "Видаліть невідомих людей, яких востаннє бачили старше ніж", 16 | "Smokedetector": "Детектор диму", 17 | "Weather station": "Метеостанція", 18 | "Welcome indoor cam": "Ласкаво просимо внутрішню/зовнішню камеру", 19 | "_additionalSettingsHeader": "Додаткові налаштування", 20 | "_realtimeEventHeader": "Події в реальному часі", 21 | "_realtimeEventInfo": "Щоб отримувати події в режимі реального часу від Welcome & Presence або отримувати сповіщення про дим у реальному часі, дверний дзвінок або датчик CO, вам потрібно вибрати екземпляр iot Adapter, який має активний помічник або віддалену ліцензію для передачі даних про події!", 22 | "auth_info_individual_credentials": "Після введення або зміни індивідуального ідентифікатора клієнта повторіть процес автентифікації за допомогою кнопки вище.", 23 | "clean minutes": "(хвилин)", 24 | "hours": "(годин)", 25 | "iotInstanceLabel": "Екземпляр iot для подій у реальному часі", 26 | "live_stream1": "Щоб збільшити інтервал оновлення або отримувати прямі трансляції від Welcome & Presence або отримувати сповіщення про дим у реальному часі, дверний дзвінок чи датчик CO, ви повинні запросити власний ключ API від Netatmo!", 27 | "live_stream2": "Для цього перейдіть за наведеною нижче URL-адресою, увійдіть у свій обліковий запис Netatmo та заповніть необхідну форму", 28 | "load minutes": "(хвилин)", 29 | "meters": "метрів", 30 | "netatmoBubendorff": "iDiamant/Bubendorff" 31 | } 32 | -------------------------------------------------------------------------------- /admin/i18n/ru/translations.json: -------------------------------------------------------------------------------- 1 | { 2 | "Authentication information": "Пожалуйста, нажмите кнопку выше, чтобы подтвердить подлинность вашей учетной записи Netatmo, чтобы позволить получить доступ к данным. Вам нужно снова аутентифицировать, когда вы добавляете больше типов устройств на свой счет, еще вы можете пропустить данные!", 3 | "CO": "Датчик угарного газа", 4 | "CheckIntervall": "Интервал обновления данных", 5 | "CleanupIntervall": "Интервал очистки данных", 6 | "ClientID": "Индивидуальный идентификатор клиента", 7 | "ClientSecret": "Индивидуальный секрет клиента", 8 | "E-Mail": "Эл. почта", 9 | "Elevation": "Высота", 10 | "Health Coach": "Тренер по здоровью", 11 | "Login with Netatmo": "Войти с Netatmo", 12 | "Netatmo App": "Из вашего приложения Netatmo", 13 | "Password": "Пароль", 14 | "RemoveEvents": "Удалить события старше", 15 | "RemoveUnknownPerson": "Удалить неизвестных лиц, которых последний раз видели старше", 16 | "Smokedetector": "детектор дыма", 17 | "Weather station": "Метеостанция", 18 | "Welcome indoor cam": "Welcome внутренняя камера", 19 | "_additionalSettingsHeader": "Дополнительные настройки", 20 | "_realtimeEventHeader": "События в реальном времени", 21 | "_realtimeEventInfo": "Чтобы получать события в реальном времени от приложения «Добро пожаловать и присутствие» или получать оповещения о дыме, дверном звонке или датчике угарного газа в реальном времени, вам необходимо выбрать экземпляр IoT-адаптера с активным помощником или удаленной лицензией для передачи данных о событиях!", 22 | "auth_info_individual_credentials": "После ввода или изменения выделенного идентификатора клиента повторите процесс аутентификации, используя кнопку выше.", 23 | "clean minutes": "(минут)", 24 | "hours": "(часы)", 25 | "iotInstanceLabel": "Инстанс iot для событий в реальном времени", 26 | "live_stream1": "Чтобы увеличить интервал обновления или получать прямые трансляции от Welcome & Присутствие или для получения оповещений о дыме в реальном времени, дверном звонке или датчике CO, вы должны запросить свой собственный ключ API от Netatmo!", 27 | "live_stream2": "Для этого перейдите по следующему URL-адресу, войдите в свою учетную запись Netatmo и заполните требуемую форму.", 28 | "load minutes": "(минут)", 29 | "meters": "метров свыше нН", 30 | "netatmoBubendorff": "iDiamant/Бубендорф" 31 | } 32 | -------------------------------------------------------------------------------- /admin/i18n/de/translations.json: -------------------------------------------------------------------------------- 1 | { 2 | "Authentication information": "Mit dem obigen Button öffnet sich ein Fenster für eine Authentifizierung mit dem Netatmo-Konto, um Zugriff auf die Daten zu ermöglichen. Dies muss wiederholt werden, wenn weitere Gerätetypen zum Netatmo-Konto hinzugefügt werden, sonst könnten Daten fehlen! Wenn sich kein Fenster öffnet bitte die Popup-Blocker-Einstellungen prüfen!", 3 | "CO": "CO-Sensor", 4 | "CheckIntervall": "Datenaktualisierungsintervall", 5 | "CleanupIntervall": "Datenbereinigungsintervall", 6 | "ClientID": "Individuelle Client-ID", 7 | "ClientSecret": "Individuelles Client-secret", 8 | "E-Mail": "Email", 9 | "Elevation": "Elevation", 10 | "Health Coach": "Smarter Luftqualitätssensor", 11 | "Login with Netatmo": "Login mit Netatmo", 12 | "Netatmo App": "Über Ihre Netatmo-App", 13 | "Password": "Passwort", 14 | "RemoveEvents": "Ereignisse entfernen, die älter sind als", 15 | "RemoveUnknownPerson": "Unbekannte Personen entfernen, die zuletzt gesehen wurden, älter als", 16 | "Smokedetector": "Rauchmelder", 17 | "Weather station": "Wetterstation", 18 | "Welcome indoor cam": "Welcome Innen-/Aussenkamera", 19 | "_additionalSettingsHeader": "Zusätzliche Einstellungen", 20 | "_realtimeEventHeader": "Echtzeit-Ereignisse", 21 | "_realtimeEventInfo": "Um Echtzeit-Ereignisse von Welcome & Presence oder Echtzeit-Rauchalarme, Türklingeln oder den CO-Sensor zu erhalten, muss eine iot Adapter-Instanz ausgewählt werden, die über eine aktive Assistenten- oder Remote-Lizenz verfügt, um die Ereignisdaten weiterzuleiten!", 22 | "auth_info_individual_credentials": "Nachdem Sie eine individuellen Client-ID eingegeben oder geändert haben, wiederholen Sie bitte den Authentifizierungsprozess mit der obigen Schaltfläche.", 23 | "clean minutes": "(Minuten)", 24 | "hours": "(Stunden)", 25 | "iotInstanceLabel": "iot-Instanz für Echtzeitereignisse", 26 | "live_stream1": "Um das Aktualisierungsintervall zu erhöhen oder Livestreams von Welcome & Anwesenheit oder um Echtzeit-Rauchalarme, Türklingel oder den CO-Sensor zu erhalten, müssen Sie Ihren eigenen API-Schlüssel von Netatmo anfordern!", 27 | "live_stream2": "Gehen Sie dazu auf die folgende URL, melden Sie sich mit Ihrem Netatmo-Konto an und füllen Sie das angeforderte Formular aus", 28 | "load minutes": "(Minuten)", 29 | "meters": "Meter über nN", 30 | "netatmoBubendorff": "iDiamant/Bubendorff" 31 | } 32 | -------------------------------------------------------------------------------- /admin/i18n/fr/translations.json: -------------------------------------------------------------------------------- 1 | { 2 | "Authentication information": "Veuillez appuyer sur le bouton ci-dessus pour vous authentifier avec votre compte Netatmo afin d'accéder aux données. Vous avez besoin d'authentifier à nouveau chaque fois que vous ajoutez d'autres types de périphériques à votre compte, sinon vous pourriez manquer des données!", 3 | "CO": "Capteur CO", 4 | "CheckIntervall": "Intervalle de mise à jour des données", 5 | "CleanupIntervall": "Intervalle de nettoyage des données", 6 | "ClientID": "ID client individuel", 7 | "ClientSecret": "Secret client individuel", 8 | "E-Mail": "E-mail", 9 | "Elevation": "Élévation", 10 | "Health Coach": "Coach Santé", 11 | "Login with Netatmo": "Connexion avec Netatmo", 12 | "Netatmo App": "Depuis votre appli Netatmo", 13 | "Password": "Mot de passe", 14 | "RemoveEvents": "Supprimer les événements antérieurs à", 15 | "RemoveUnknownPerson": "Supprimer les personnes inconnues vues pour la dernière fois plus de", 16 | "Smokedetector": "détecteur de fumée", 17 | "Weather station": "Station météo", 18 | "Welcome indoor cam": "Caméra intérieure/extérieure Welcome", 19 | "_additionalSettingsHeader": "Paramètres additionnels", 20 | "_realtimeEventHeader": "Événements en temps réel", 21 | "_realtimeEventInfo": "Afin de recevoir des événements en temps réel de Welcome & Presence ou d'obtenir des alertes de fumée en temps réel, une sonnette ou le capteur de CO, vous devez sélectionner une instance d'adaptateur iot disposant d'un assistant actif ou d'une licence à distance pour transmettre les données d'événement !", 22 | "auth_info_individual_credentials": "Après avoir saisi ou modifié un ID client dédié, veuillez répéter le processus d'authentification à l'aide du bouton ci-dessus.", 23 | "clean minutes": "(minutes)", 24 | "hours": "(les heures)", 25 | "iotInstanceLabel": "Instance iot pour les événements en temps réel", 26 | "live_stream1": "Afin d'augmenter l'intervalle de mise à jour ou de recevoir des flux en direct de Welcome & Présence ou pour recevoir les alertes fumées en temps réel, la sonnette ou le capteur de CO, il faut demander sa propre clé API à Netatmo !", 27 | "live_stream2": "Pour cela, rendez-vous sur l'URL suivante, connectez-vous avec votre compte Netatmo et remplissez le formulaire demandé", 28 | "load minutes": "(minutes)", 29 | "meters": "mètres sur nN", 30 | "netatmoBubendorff": "iDiamant/Bubendorff" 31 | } 32 | -------------------------------------------------------------------------------- /lib/eventEmitterBridge.js: -------------------------------------------------------------------------------- 1 | const EventEmitter = require('events'); 2 | 3 | class EventEmitterBridge extends EventEmitter { 4 | constructor(api, adapter) { 5 | // Simulate a singleton 6 | if (EventEmitterBridge._instance) { 7 | return EventEmitterBridge._instance; 8 | } 9 | super(); 10 | EventEmitterBridge._instance = this; 11 | this.homeIds = []; 12 | 13 | if (!adapter.config.iotInstance) { 14 | adapter.log.warn('Disable Realtime Events because no iot instance configured. Please see Adapter Readme for Details!'); 15 | return; 16 | } 17 | this.adapter = adapter; 18 | this.apiInstance = api; 19 | adapter.sendTo(adapter.config.iotInstance, 'getServiceEndpoint', {serviceName: 'netatmo'}, result => { 20 | if (result && result.error) { 21 | adapter.log.error(`Cannot get service endpoint for callbacks: ${result.error}`); 22 | } 23 | if (!result || !result.url || !result.stateID) { 24 | adapter.log.error(`Cannot get service endpoint for callbacks: ${JSON.stringify(result)}`); 25 | return; 26 | } 27 | this.callbackUrl = result.url; 28 | this.stateId = result.stateID; 29 | adapter.subscribeForeignStates(this.stateId); 30 | 31 | adapter.on('stateChange', (id, state) => { 32 | if (id === this.stateId && state && state.val && state.ack) { 33 | try { 34 | this.adapter.log.debug(`Websocket incoming alert: ${state.val}`) 35 | const obj = JSON.parse(state.val); 36 | if (obj && this.homeIds.includes(obj.home_id)) { 37 | this.emit('alert', obj); 38 | } 39 | } catch (e) { 40 | adapter.log.error(`Cannot parse callback data: ${e.message}`); 41 | } 42 | } 43 | }); 44 | 45 | this.apiInstance.addWebHook(this.callbackUrl, (err, body, qs) => { 46 | if (err) { 47 | this.adapter.log.error(`Error while adding webhook: ${err}`); 48 | } else { 49 | this.adapter.log.info(`Webhook added: ${JSON.stringify(body.toString())}`); 50 | } 51 | }); 52 | }); 53 | } 54 | 55 | joinHome(id) { 56 | if (!this.homeIds.includes(id)) { 57 | this.homeIds.push(id); 58 | } 59 | } 60 | 61 | destructor() { 62 | if (this.stateId) { 63 | this.adapter.unsubscribeForeignStates(this.stateId); 64 | } 65 | this.apiInstance = null; 66 | this.stateId = null; 67 | this.callbackUrl = null; 68 | } 69 | } 70 | 71 | module.exports = EventEmitterBridge; 72 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Configuration for probot-stale - https://github.com/probot/stale 2 | 3 | # Number of days of inactivity before an Issue or Pull Request becomes stale 4 | daysUntilStale: 90 5 | 6 | # Number of days of inactivity before an Issue or Pull Request with the stale label is closed. 7 | # Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale. 8 | daysUntilClose: 7 9 | 10 | # Only issues or pull requests with all of these labels are check if stale. Defaults to `[]` (disabled) 11 | onlyLabels: [] 12 | 13 | # Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable 14 | exemptLabels: 15 | - enhancement 16 | - security 17 | - bug 18 | 19 | # Set to true to ignore issues in a project (defaults to false) 20 | exemptProjects: true 21 | 22 | # Set to true to ignore issues in a milestone (defaults to false) 23 | exemptMilestones: true 24 | 25 | # Set to true to ignore issues with an assignee (defaults to false) 26 | exemptAssignees: false 27 | 28 | # Label to use when marking as stale 29 | staleLabel: wontfix 30 | 31 | # Comment to post when marking as stale. Set to `false` to disable 32 | markComment: > 33 | This issue has been automatically marked as stale because it has not had 34 | recent activity. It will be closed if no further activity occurs within the next 7 days. 35 | Please check if the issue is still relevant in the most current version of the adapter 36 | and tell us. Also check that all relevant details, logs and reproduction steps 37 | are included and update them if needed. 38 | Thank you for your contributions. 39 | 40 | Dieses Problem wurde automatisch als veraltet markiert, da es in letzter Zeit keine Aktivitäten gab. 41 | Es wird geschlossen, wenn nicht innerhalb der nächsten 7 Tage weitere Aktivitäten stattfinden. 42 | Bitte überprüft, ob das Problem auch in der aktuellsten Version des Adapters noch relevant ist, 43 | und teilt uns dies mit. Überprüft auch, ob alle relevanten Details, Logs und Reproduktionsschritte 44 | enthalten sind bzw. aktualisiert diese. 45 | Vielen Dank für Eure Unterstützung. 46 | 47 | # Comment to post when removing the stale label. 48 | # unmarkComment: > 49 | # Your comment here. 50 | 51 | # Comment to post when closing a stale Issue or Pull Request. 52 | closeComment: > 53 | This issue has been automatically closed because of inactivity. Please open a new 54 | issue if still relevant and make sure to include all relevant details, logs and 55 | reproduction steps. 56 | Thank you for your contributions. 57 | 58 | Dieses Problem wurde aufgrund von Inaktivität automatisch geschlossen. Bitte öffnet ein 59 | neues Issue, falls dies noch relevant ist und stellt sicher das alle relevanten Details, 60 | Logs und Reproduktionsschritte enthalten sind. 61 | Vielen Dank für Eure Unterstützung. 62 | 63 | # Limit the number of actions per hour, from 1-30. Default is 30 64 | limitPerRun: 30 65 | 66 | # Limit to only `issues` or `pulls` 67 | only: issues 68 | 69 | # Optionally, specify configuration settings that are specific to just 'issues' or 'pulls': 70 | # pulls: 71 | # daysUntilStale: 30 72 | # markComment: > 73 | # This pull request has been automatically marked as stale because it has not had 74 | # recent activity. It will be closed if no further activity occurs. Thank you 75 | # for your contributions. 76 | 77 | # issues: 78 | # exemptLabels: 79 | # - confirmed 80 | -------------------------------------------------------------------------------- /.github/workflows/test-and-release.yml: -------------------------------------------------------------------------------- 1 | # This is a composition of lint and test scripts 2 | # Make sure to update this file along with the others 3 | 4 | name: Test and Release 5 | 6 | # Run this job on all pushes and pull requests 7 | # as well as tags with a semantic version 8 | on: 9 | push: 10 | branches: 11 | - '*' 12 | tags: 13 | # normal versions 14 | - "v?[0-9]+.[0-9]+.[0-9]+" 15 | # pre-releases 16 | - "v?[0-9]+.[0-9]+.[0-9]+-**" 17 | pull_request: {} 18 | 19 | # Cancel previous PR/branch runs when a new commit is pushed 20 | concurrency: 21 | group: ${{ github.ref }} 22 | cancel-in-progress: true 23 | 24 | jobs: 25 | # Performs quick checks before the expensive test runs 26 | check-and-lint: 27 | if: contains(github.event.head_commit.message, '[skip ci]') == false 28 | 29 | runs-on: ubuntu-latest 30 | 31 | strategy: 32 | matrix: 33 | node-version: [22.x] 34 | 35 | steps: 36 | - uses: actions/checkout@v3 37 | - name: Use Node.js ${{ matrix.node-version }} 38 | uses: actions/setup-node@v3 39 | with: 40 | node-version: ${{ matrix.node-version }} 41 | 42 | 43 | - name: Install Dependencies 44 | run: npm install 45 | 46 | # - name: Perform a type check 47 | # run: npm run check:ts 48 | # env: 49 | # CI: true 50 | # - name: Lint TypeScript code 51 | # run: npm run lint 52 | - name: Test package files 53 | run: npm run test:package 54 | 55 | # Runs adapter tests on all supported node versions and OSes 56 | adapter-tests: 57 | if: contains(github.event.head_commit.message, '[skip ci]') == false 58 | 59 | needs: [check-and-lint] 60 | 61 | runs-on: ${{ matrix.os }} 62 | strategy: 63 | matrix: 64 | node-version: [18.x, 20.x, 22.x, 24.x] 65 | os: [ubuntu-latest, windows-latest, macos-latest] 66 | 67 | steps: 68 | - uses: actions/checkout@v3 69 | - name: Use Node.js ${{ matrix.node-version }} 70 | uses: actions/setup-node@v3 71 | with: 72 | node-version: ${{ matrix.node-version }} 73 | 74 | - name: Install Dependencies 75 | run: npm install 76 | 77 | # - name: Run local tests 78 | # run: npm test 79 | # - name: Run unit tests 80 | # run: npm run test:unit 81 | - name: Run integration tests # (linux/osx) 82 | if: startsWith(runner.OS, 'windows') == false 83 | run: DEBUG=testing:* npm run test:integration 84 | - name: Run integration tests # (windows) 85 | if: startsWith(runner.OS, 'windows') 86 | run: set DEBUG=testing:* & npm run test:integration 87 | 88 | # Deploys the final package to NPM 89 | deploy: 90 | needs: [adapter-tests] 91 | 92 | # Trigger this step only when a commit on master is tagged with a version number 93 | if: | 94 | contains(github.event.head_commit.message, '[skip ci]') == false && 95 | github.event_name == 'push' && 96 | startsWith(github.ref, 'refs/tags/') 97 | runs-on: ubuntu-latest 98 | strategy: 99 | matrix: 100 | node-version: [22.x] 101 | 102 | steps: 103 | - name: Checkout code 104 | uses: actions/checkout@v3 105 | 106 | - name: Use Node.js ${{ matrix.node-version }} 107 | uses: actions/setup-node@v3 108 | with: 109 | node-version: ${{ matrix.node-version }} 110 | 111 | - name: Extract the version and commit body from the tag 112 | id: extract_release 113 | # The body may be multiline, therefore we need to escape some characters 114 | run: | 115 | VERSION="${{ github.ref }}" 116 | VERSION=${VERSION##*/} 117 | VERSION=${VERSION##*v} 118 | echo "::set-output name=VERSION::$VERSION" 119 | BODY=$(git show -s --format=%b) 120 | BODY="${BODY//'%'/'%25'}" 121 | BODY="${BODY//$'\n'/'%0A'}" 122 | BODY="${BODY//$'\r'/'%0D'}" 123 | echo "::set-output name=BODY::$BODY" 124 | 125 | - name: Install Dependencies 126 | run: npm install 127 | 128 | # - name: Create a clean build 129 | # run: npm run build 130 | - name: Publish package to npm 131 | run: | 132 | npm config set //registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }} 133 | npm whoami 134 | npm publish 135 | 136 | - name: Create Github Release 137 | uses: actions/create-release@v1 138 | env: 139 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 140 | with: 141 | tag_name: ${{ github.ref }} 142 | release_name: Release v${{ steps.extract_release.outputs.VERSION }} 143 | draft: false 144 | # Prerelease versions create prereleases on Github 145 | prerelease: ${{ contains(steps.extract_release.outputs.VERSION, '-') }} 146 | body: ${{ steps.extract_release.outputs.BODY }} 147 | 148 | - name: Notify Sentry.io about the release 149 | run: | 150 | npm i -g @sentry/cli 151 | export SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} 152 | export SENTRY_URL=https://sentry.iobroker.net 153 | export SENTRY_ORG=iobroker 154 | export SENTRY_PROJECT=iobroker-netatmo 155 | export SENTRY_VERSION=iobroker.netatmo@${{ steps.extract_release.outputs.VERSION }} 156 | sentry-cli releases new $SENTRY_VERSION 157 | sentry-cli releases finalize $SENTRY_VERSION 158 | 159 | # sentry-cli releases set-commits $SENTRY_VERSION --auto 160 | # Add the following line BEFORE finalize if sourcemap uploads are needed 161 | # sentry-cli releases files $SENTRY_VERSION upload-sourcemaps build/ 162 | -------------------------------------------------------------------------------- /admin/jsonConfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "panel", 3 | "i18n": true, 4 | "items": { 5 | "netatmoCoach": { 6 | "type": "checkbox", 7 | "label": "Health Coach", 8 | "sm": 12, 9 | "md": 6, 10 | "lg": 3 11 | }, 12 | "netatmoWeather": { 13 | "type": "checkbox", 14 | "label": "Weather station", 15 | "sm": 12, 16 | "md": 6, 17 | "lg": 3 18 | }, 19 | "netatmoWelcome": { 20 | "type": "checkbox", 21 | "label": "Welcome indoor cam", 22 | "sm": 12, 23 | "md": 6, 24 | "lg": 3 25 | }, 26 | "netatmoSmokedetector": { 27 | "type": "checkbox", 28 | "label": "Smokedetector", 29 | "disabled": "data.id === '' || data.secret === ''", 30 | "sm": 12, 31 | "md": 6, 32 | "lg": 3 33 | }, 34 | "netatmoCOSensor": { 35 | "type": "checkbox", 36 | "label": "netatmoCOSensor", 37 | "disabled": "data.id === '' || data.secret === ''", 38 | "sm": 12, 39 | "md": 6, 40 | "lg": 3 41 | }, 42 | "netatmoDoorBell": { 43 | "type": "checkbox", 44 | "label": "netatmoDoorBell", 45 | "disabled": "data.id === '' || data.secret === ''", 46 | "sm": 12, 47 | "md": 6, 48 | "lg": 3 49 | }, 50 | "netatmoBubendorff": { 51 | "type": "checkbox", 52 | "label": "netatmoBubendorff", 53 | "sm": 12, 54 | "md": 6, 55 | "lg": 3 56 | }, 57 | "_authenticate": { 58 | "newLine": true, 59 | "variant": "contained", 60 | "color": "primary", 61 | "disabled": "!_alive", 62 | "type": "sendTo", 63 | "error": { 64 | "connect timeout": "Connection timeout" 65 | }, 66 | "icon": "auth", 67 | "command": "getOAuthStartLink", 68 | "jsonData": "{\"client_id\": \"${data.id}\",\"client_secret\": \"${data.secret}\",\"redirect_uri_base\": \"${data._origin}\", \"scopes\": {\"netatmoCoach\": ${data.netatmoCoach}, \"netatmoWeather\": ${data.netatmoWeather}, \"netatmoWelcome\": ${data.netatmoWelcome}, \"netatmoSmokedetector\": ${data.netatmoSmokedetector}, \"netatmoCOSensor\": ${data.netatmoCOSensor}, \"netatmoDoorBell\": ${data.netatmoDoorBell}, \"netatmoBubendorff\": ${data.netatmoBubendorff} }}", 69 | "label": "Authenticate with Netatmo", 70 | "openUrl": true, 71 | "window": "Login with Netatmo" 72 | }, 73 | "_authinfo": { 74 | "type": "header", 75 | "size": 4, 76 | "style": { 77 | "marginTop": 20 78 | }, 79 | "sm": 12, 80 | "text": "Authentication information" 81 | }, 82 | "_text1": { 83 | "type": "staticText", 84 | "newLine": true, 85 | "text": "live_stream1" 86 | }, 87 | "_text2": { 88 | "type": "staticText", 89 | "newLine": true, 90 | "text": "live_stream2" 91 | }, 92 | "_link": { 93 | "newLine": true, 94 | "type": "staticLink", 95 | "text": "https://dev.netatmo.com/apps/createanapp", 96 | "href": "https://auth.netatmo.com/access/login?next_url=https%3A%2F%2Fdev.netatmo.com%2Fapps%2Fcreateanapp" 97 | }, 98 | "_text3": { 99 | "type": "staticText", 100 | "newLine": true, 101 | "text": "auth_info_individual_credentials" 102 | }, 103 | "id": { 104 | "type": "text", 105 | "newLine": true, 106 | "label": "ClientID", 107 | "help": "Netatmo App", 108 | "sm": 6, 109 | "lg": 3 110 | }, 111 | "secret": { 112 | "type": "password", 113 | "repeat": false, 114 | "help": "Netatmo App", 115 | "label": "ClientSecret", 116 | "sm": 6, 117 | "lg": 3 118 | }, 119 | "_realtimeEventHeader": { 120 | "hidden": "!data.netatmoWelcome && !data.netatmoSmokedetector && !data.netatmoCOSensor && !data.netatmoDoorBell", 121 | "type": "header", 122 | "newLine": true, 123 | "size": 4, 124 | "style": { 125 | "marginTop": 20 126 | }, 127 | "sm": 12, 128 | "text": "_realtimeEventHeader" 129 | }, 130 | "_realtimeEventInfo": { 131 | "hidden": "!data.netatmoWelcome && !data.netatmoSmokedetector && !data.netatmoCOSensor && !data.netatmoDoorBell", 132 | "type": "staticText", 133 | "newLine": true, 134 | "text": "_realtimeEventInfo" 135 | }, 136 | "iotInstance": { 137 | "hidden": "!data.netatmoWelcome && !data.netatmoSmokedetector && !data.netatmoCOSensor && !data.netatmoDoorBell", 138 | "newLine": true, 139 | "type": "instance", 140 | "adapter": "iot", 141 | "label": "iotInstanceLabel", 142 | "help": "hours", 143 | "sm": 6, 144 | "lg": 3 145 | }, 146 | "_additionalSettingsHeader": { 147 | "type": "header", 148 | "newLine": true, 149 | "size": 4, 150 | "style": { 151 | "marginTop": 20 152 | }, 153 | "sm": 12, 154 | "text": "_additionalSettingsHeader" 155 | }, 156 | "location_elevation": { 157 | "hidden": "!data.netatmoWeather && !data.netatmoCoach", 158 | "newLine": true, 159 | "type": "number", 160 | "label": "Elevation", 161 | "help": "meters", 162 | "sm": 6, 163 | "lg": 3 164 | }, 165 | "check_interval": { 166 | "newLine": true, 167 | "type": "number", 168 | "label": "CheckIntervall", 169 | "help": "load minutes", 170 | "sm": 6, 171 | "lg": 3, 172 | "min": 1 173 | }, 174 | "cleanup_interval": { 175 | "hidden": "!data.netatmoWelcome && !data.netatmoSmokedetector && !data.netatmoCOSensor && !data.netatmoDoorBell", 176 | "type": "number", 177 | "label": "CleanupIntervall", 178 | "help": "clean minutes", 179 | "sm": 6, 180 | "lg": 3, 181 | "min": 1 182 | }, 183 | "event_time": { 184 | "hidden": "!data.netatmoWelcome && !data.netatmoSmokedetector && !data.netatmoCOSensor && !data.netatmoDoorBell", 185 | "newLine": true, 186 | "type": "number", 187 | "label": "RemoveEvents", 188 | "help": "clean minutes", 189 | "sm": 6, 190 | "lg": 3 191 | }, 192 | "unknown_person_time": { 193 | "hidden": "!data.netatmoWelcome", 194 | "type": "number", 195 | "label": "RemoveUnknownPerson", 196 | "help": "hours", 197 | "sm": 6, 198 | "lg": 3 199 | } 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /lib/netatmoBubendorff.js: -------------------------------------------------------------------------------- 1 | module.exports = function (myapi, myadapter) { 2 | const api = myapi; 3 | const adapter = myadapter; 4 | 5 | let homeIds = []; 6 | let moduleIds = []; 7 | 8 | let updateTimer = null; 9 | let finalized = false; 10 | 11 | let that = null; 12 | 13 | this.init = function () { 14 | that = this; 15 | }; 16 | 17 | this.finalize = function () { 18 | finalized = true; 19 | if (updateTimer) { 20 | clearTimeout(updateTimer); 21 | updateTimer = null; 22 | } 23 | }; 24 | 25 | this.situativeUpdate = function (homeId, moduleId) { 26 | if (finalized) return; 27 | if (homeIds.includes(homeId) && moduleIds.includes(moduleId)) { 28 | updateTimer && clearTimeout(updateTimer); 29 | updateTimer = setTimeout(async () => { 30 | await that.requestUpdateBubendorff(true); 31 | updateTimer && clearTimeout(updateTimer); 32 | updateTimer = setTimeout(async () => { 33 | updateTimer && clearTimeout(updateTimer); 34 | updateTimer = null; 35 | await that.requestUpdateBubendorff(true); 36 | }, 15000); 37 | }, 2000); 38 | } 39 | } 40 | 41 | this.requestUpdateBubendorff = function (ignoreTimer) { 42 | return new Promise(resolve => { 43 | if (updateTimer && !ignoreTimer) { 44 | adapter.log.debug('Update already scheduled'); 45 | resolve(); 46 | return; 47 | } 48 | 49 | api.homedataExtended({ 50 | gateway_types: 'NBG' 51 | }, async (err, data) => { 52 | if (finalized) return; 53 | if (err !== null) { 54 | adapter.log.error(err); 55 | } else { 56 | const homes = data.homes; 57 | homeIds = []; 58 | moduleIds = []; 59 | 60 | if (Array.isArray(homes)) { 61 | for (let h = 0; h < homes.length; h++) { 62 | const aHome = homes[h]; 63 | if (!aHome.modules) continue; 64 | adapter.log.debug(`Get Shutter for Home ${h}: ${JSON.stringify(aHome)}`); 65 | 66 | await handleHome(aHome); 67 | } 68 | } 69 | } 70 | resolve(); 71 | }); 72 | }); 73 | }; 74 | 75 | /* 76 | function formatName(aHomeName) { 77 | return aHomeName.replace(/ /g, '-').replace(/---/g, '-').replace(/--/g, '-').replace(adapter.FORBIDDEN_CHARS, '_').replace(/\s|\./g, '_'); 78 | } 79 | */ 80 | 81 | async function handleHome(aHome) { 82 | const homeId = aHome.id.replace(/:/g, '-'); //formatName(aHome.name); 83 | 84 | homeIds.push(aHome.id); 85 | 86 | await adapter.extendOrSetObjectNotExistsAsync(homeId, { 87 | type: 'folder', 88 | common: { 89 | name: aHome.name || aHome.id, 90 | }, 91 | native: { 92 | id: aHome.id, 93 | }, 94 | }); 95 | 96 | if (aHome.modules) { 97 | for (const aRollerShutter of aHome.modules) { 98 | if (aRollerShutter.id) { 99 | moduleIds.push(aRollerShutter.id); 100 | await handleRollerShutter(aRollerShutter, aHome); 101 | } 102 | } 103 | } 104 | } 105 | 106 | async function handleRollerShutter(aRollerShutter, aHome) { 107 | const aParent = aHome.id.replace(/:/g, '-'); // formatName(aHome.name); 108 | const aParentRooms = aHome.rooms; 109 | const fullPath = `${aParent}.${aRollerShutter.id.replace(/:/g, '-')}`; 110 | const infoPath = `${fullPath}.info`; 111 | 112 | await adapter.extendOrSetObjectNotExistsAsync(fullPath, { 113 | type: 'device', 114 | common: { 115 | name: aRollerShutter.name || aRollerShutter.id, 116 | }, 117 | native: { 118 | id: aRollerShutter.id, 119 | type: aRollerShutter.type, 120 | bridge: aRollerShutter.bridge, 121 | } 122 | }); 123 | 124 | await adapter.extendOrSetObjectNotExistsAsync(infoPath, { 125 | type: 'channel', 126 | common: { 127 | name: `${aRollerShutter.name || aRollerShutter.id} Info`, 128 | }, 129 | native: { 130 | } 131 | }); 132 | 133 | if (aRollerShutter.id) { 134 | await adapter.extendOrSetObjectNotExistsAsync(`${infoPath}.id`, { 135 | type: 'state', 136 | common: { 137 | name: `${aRollerShutter.name || aRollerShutter.id} ID`, 138 | type: 'string', 139 | role: 'state', 140 | read: true, 141 | write: false 142 | } 143 | }); 144 | await adapter.setStateAsync(`${infoPath}.id`, {val: aRollerShutter.id, ack: true}); 145 | } 146 | 147 | if (aRollerShutter.setup_date) { 148 | await adapter.extendOrSetObjectNotExistsAsync(`${infoPath}.setup_date`, { 149 | type: 'state', 150 | common: { 151 | name: `${aRollerShutter.name || aRollerShutter.id} Setup date`, 152 | type: 'string', 153 | role: 'state', 154 | read: true, 155 | write: false 156 | } 157 | }); 158 | 159 | await adapter.setStateAsync(`${infoPath}.setup_date`, { 160 | val: new Date(aRollerShutter.setup_date * 1000).toString(), 161 | ack: true 162 | }); 163 | } 164 | 165 | if (aRollerShutter.name) { 166 | await adapter.extendOrSetObjectNotExistsAsync(`${infoPath}.name`, { 167 | type: 'state', 168 | common: { 169 | name: `${aRollerShutter.name || aRollerShutter.id} Name`, 170 | type: 'string', 171 | role: 'state', 172 | read: true, 173 | write: false 174 | } 175 | }); 176 | 177 | await adapter.setStateAsync(`${infoPath}.name`, {val: aRollerShutter.name, ack: true}); 178 | } 179 | 180 | if (aRollerShutter.wifi_strength) { 181 | await adapter.extendOrSetObjectNotExistsAsync(`${infoPath}.wifi_strength`, { 182 | type: 'state', 183 | common: { 184 | name: `${aRollerShutter.name || aRollerShutter.id} Wifi Strength`, 185 | type: 'number', 186 | role: 'state', 187 | read: true, 188 | write: false 189 | } 190 | }); 191 | 192 | await adapter.setStateAsync(`${infoPath}.wifi_strength`, {val: aRollerShutter.wifi_strength, ack: true}); 193 | } 194 | 195 | await adapter.extendOrSetObjectNotExistsAsync(`${infoPath}.isBridge`, { 196 | type: 'state', 197 | common: { 198 | name: `${aRollerShutter.name || aRollerShutter.id} Is Bridge?`, 199 | type: 'boolean', 200 | role: 'state', 201 | read: true, 202 | write: false 203 | } 204 | }); 205 | 206 | await adapter.setStateAsync(`${infoPath}.isBridge`, {val: !!aRollerShutter.modules_bridged, ack: true}); 207 | 208 | if (aRollerShutter.room_id) { 209 | const roomName = aParentRooms.find((r) => r.id === aRollerShutter.room_id) 210 | if (roomName) { 211 | await adapter.extendOrSetObjectNotExistsAsync(`${infoPath}.room`, { 212 | type: 'state', 213 | common: { 214 | name: `${aRollerShutter.name || aRollerShutter.id} Room`, 215 | type: 'string', 216 | role: 'state', 217 | read: true, 218 | write: false 219 | } 220 | }); 221 | 222 | await adapter.setStateAsync(`${infoPath}.room`, {val: roomName.name, ack: true}); 223 | } 224 | } 225 | 226 | if (!aRollerShutter.modules_bridged) { 227 | await adapter.extendOrSetObjectNotExistsAsync(`${fullPath}.currentPosition`, { 228 | type: 'state', 229 | common: { 230 | name: `${aRollerShutter.name || aRollerShutter.id} Current Position`, 231 | type: 'number', 232 | role: 'state', 233 | read: true, 234 | write: false 235 | } 236 | }); 237 | await adapter.setStateAsync(`${fullPath}.currentPosition`, {val: aRollerShutter.current_position, ack: true}); 238 | 239 | await adapter.extendOrSetObjectNotExistsAsync(`${fullPath}.targetPosition`, { 240 | type: 'state', 241 | common: { 242 | name: `${aRollerShutter.name || aRollerShutter.id} Target Position`, 243 | type: 'number', 244 | role: 'state', 245 | read: true, 246 | write: true, 247 | min: -2, 248 | max: 100, 249 | step: aRollerShutter['target_position:step'] 250 | }, 251 | native: { 252 | homeId: aHome.id, 253 | moduleId: aRollerShutter.id, 254 | bridgeId: aRollerShutter.bridge, 255 | field: 'target_position' 256 | } 257 | }); 258 | await adapter.setStateAsync(`${fullPath}.targetPosition`, {val: aRollerShutter.target_position, ack: true}); 259 | 260 | await adapter.extendOrSetObjectNotExistsAsync(`${fullPath}.open`, { 261 | type: 'state', 262 | common: { 263 | name: `${aRollerShutter.name || aRollerShutter.id} Open`, 264 | type: 'boolean', 265 | role: 'button', 266 | read: false, 267 | write: true, 268 | }, 269 | native: { 270 | homeId: aHome.id, 271 | moduleId: aRollerShutter.id, 272 | bridgeId: aRollerShutter.bridge, 273 | field: 'target_position', 274 | setValue: 100 275 | } 276 | }); 277 | await adapter.setStateAsync(`${fullPath}.open`, {val: false, ack: true}); 278 | 279 | await adapter.extendOrSetObjectNotExistsAsync(`${fullPath}.close`, { 280 | type: 'state', 281 | common: { 282 | name: `${aRollerShutter.name || aRollerShutter.id} Close`, 283 | type: 'boolean', 284 | role: 'button', 285 | read: false, 286 | write: true, 287 | }, 288 | native: { 289 | homeId: aHome.id, 290 | moduleId: aRollerShutter.id, 291 | bridgeId: aRollerShutter.bridge, 292 | field: 'target_position', 293 | setValue: 0 294 | } 295 | }); 296 | await adapter.setStateAsync(`${fullPath}.close`, {val: false, ack: true}); 297 | 298 | await adapter.extendOrSetObjectNotExistsAsync(`${fullPath}.stop`, { 299 | type: 'state', 300 | common: { 301 | name: `${aRollerShutter.name || aRollerShutter.id} Stop`, 302 | type: 'boolean', 303 | role: 'button', 304 | read: false, 305 | write: true, 306 | }, 307 | native: { 308 | homeId: aHome.id, 309 | moduleId: aRollerShutter.id, 310 | bridgeId: aRollerShutter.bridge, 311 | field: 'target_position', 312 | setValue: -1 313 | } 314 | }); 315 | await adapter.setStateAsync(`${fullPath}.stop`, {val: false, ack: true}); 316 | } 317 | 318 | } 319 | 320 | } 321 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Logo](admin/netatmo.png) 2 | # ioBroker.netatmo 3 | 4 | ![Number of Installations](http://iobroker.live/badges/netatmo-installed.svg) 5 | ![Number of Installations](http://iobroker.live/badges/netatmo-stable.svg) 6 | [![NPM version](http://img.shields.io/npm/v/iobroker.netatmo.svg)](https://www.npmjs.com/package/iobroker.netatmo) 7 | 8 | ![Test and Release](https://github.com/PArns/iobroker.netatmo/workflows/Test%20and%20Release/badge.svg) 9 | [![Translation status](https://weblate.iobroker.net/widgets/adapters/-/netatmo/svg-badge.svg)](https://weblate.iobroker.net/engage/adapters/?utm_source=widget) 10 | [![Downloads](https://img.shields.io/npm/dm/iobroker.netatmo.svg)](https://www.npmjs.com/package/iobroker.netatmo) 11 | 12 | **This adapter uses Sentry libraries to automatically report exceptions and code errors to the developers.** For more details and for information how to disable the error reporting see [Sentry-Plugin Documentation](https://github.com/ioBroker/plugin-sentry#plugin-sentry)! Sentry reporting is used starting with js-controller 3.0. 13 | 14 | Netatmo adapter for ioBroker 15 | 16 | ## __Important Note for Realtime events (Doorbell, Welcome, Presence, CO2/Smoke-Alarm)__ 17 | To receive realtime events from Netatmo you need an iot/Pro-Cloud Account with an Assistent- or Remote-License and an installed iot Instance connected to this account. The iot Instance needs to have v1.14.0 or higher. 18 | 19 | Please select the iot Instance in the adapter settings and restart the adapter. 20 | 21 | Netatmo adapter versions < 3.0 used an heroku service to pass these webhook events through, but Heroku has deceased this free service. So all Netatmo versions < 3.0 will not get realtime events anymore since 28.11.2022! Because of this we decided for this way to use proofed and stable iot/Pro-Cloud services. 22 | 23 | ## __Important Note for Authentication changes October 2022__ 24 | According to Netatmo the "old" way to authenticate with username and password directly by entering them into the adapter will be disabled by October 2022. 25 | 26 | Version 2.0 of the adapter addresses this change and adjust the authentication. All upgrades before October 2022 should allow a seamless upgrade to 2.0.0 on the first start automatically - else requires a new authentication. 27 | 28 | ## __Important Note for v2.0.0!__ 29 | With v 2.0 of the adapter the object structure will change completely! Instead of names we decided to better use the unique IDs to make sure that duplicate or changing names do not produce issues. 30 | 31 | 32 | ## Installation and Configuration 33 | You need to authenticate with your NetAtmo account using the Adapter Admin UI. 34 | 35 | First select all relevant device types you want to sync data for. When you change them you need to do the Authentication again later. 36 | 37 | If you want to use a dedicated client-id/secret (see below) you can also enter them before the Authentication. 38 | 39 | Use the "Authenticate with Netatmo" Button to start the authentication flow. A new Windows/Tab will be opened with the Netatmo Login page. After logging in and acknowledging the data access you are redirected back to your admin page. 40 | 41 | In case of success just close the window and reload the adapter configuration. In case of an error check the error message and try again 42 | 43 | By default, a general API key is used to do the requests which limits the update interval to 10 Minutes! 44 | 45 | To increase the interval or to get live updates from Welcome & Presence, CO- und Smoke-Detectors are only you need to enter an own ID/Secret from your NetAtmo App. 46 | To do so, go to the following URL, login with your Netatmo account and fill out the requested form on https://auth.netatmo.com/access/login?next_url=https%3A%2F%2Fdev.netatmo.com%2Fapps%2Fcreateanapp ! 47 | 48 | Please make sure to configure your limits that they respect https://dev.netatmo.com/guideline#rate-limits (and have in mind that these limits also exist for ALL USERS if you do not use an own ID/Secret) 49 | 50 | ## Usage 51 | The adapter should query all device types that you enabled in the configuration. If you change this you need to Re-Do the "Authenticate with Netatmo". 52 | 53 | The adapter then creates states with data of the devices and extra "event" states for the devices that support this. To receive these events you need to choose the iot Instance and add a pro Cloud Account (see above). 54 | 55 | Some devices are initialized with the latest event per type (if it happened in the last time), e.g. the cameras. For other device types (e.g. smoke/co2 sensors) the events are not pre-filled from the past and these states will be filled as soon as the next event is received. 56 | 57 | ### Special note for iDiamant/Bubendorff Roller shutters 58 | The Netatmo API do not provide real time data for changes to the roller shutter devices. This means that the data are polled defined in the polling interval. 59 | This basically means that the data will not be accurate in real time when the rollershutter are controller directly or via the Netatmo App. 60 | 61 | When the devices are controlled via the adapter, it will update the values 2s and 17s after the controlling so that the data could be more up-to-date. 62 | 63 | Depending on the device The target position can be set to any number between 0% and 100% OR only to 0% or 100% (and -1 for stop). But for these actions also the convenient buttons open, close and stop can be used. 64 | 65 | ## sendTo support 66 | 67 | ### setAway 68 | You can also use the sendTo command to set all persons as away (for example if in use as alarm system) 69 | ``` 70 | sendTo('netatmo.0', "setAway", {homeId: '1234567890abcdefg'}); 71 | ``` 72 | or 73 | ``` 74 | sendTo('netatmo.0', "setAway"); 75 | ``` 76 | to mark all persons as away for all cameras 77 | 78 | it's also possible to mark one or more specific persons as away 79 | ``` 80 | sendTo('netatmo.0', "setAway", {homeId: '1234567890abcdefg', personsId: ['123123123123123']}); 81 | ``` 82 | 83 | The parameter homeId is the string listed behind the name of your Camera within the Objects tab (optional, if multiple cameras are installed), 84 | the personsId is the id within the "Known" persons folder 85 | 86 | ### setHome 87 | Basically the same functionality as described for "setAway" above also is existing for "setHome" to set persons or full homes as "occupied". 88 | 89 | 93 | ## Changelog 94 | 95 | ### __WORK IN PROGRESS__ 96 | * (@Apollon77) Removed the usage of a deprecated API from Netatmo which was used to enrich person data 97 | * (@Apollon77) Added product type NACamDoorTag 98 | * (@Apollon77) Allows value -2 for target position of Bubendorff roller shutters to put a Bubendorff shutter with jalousieable slats in jalousie mode 99 | 100 | ### 3.1.0 (2023-01-06) 101 | * (Apollon77) Add support for Bubendorff roller shutters 102 | * (Apollon77) Fix Monitoring State for Welcomes 103 | * (Apollon77) Allow to just use CO2/Smoke sensors 104 | * (Apollon77) Optimize Shutdown procedure 105 | 106 | ### 3.0.0 (2022-12-14) 107 | * (Apollon77/bluefox) BREAKING CHANGE: Restructure Realtime events to be received via iot instance (iot >= 1.14.0 required) 108 | 109 | ### 2.1.2 (2022-11-17) 110 | * (bluefox) Added missing objects for `Welcome` devices 111 | 112 | ### 2.1.1 (2022-09-30) 113 | * (Apollon77) Make sure device types that require custom credentials are not selectable in UI without entering them 114 | * (Apollon77) Fix a potential crash case 115 | 116 | ### 2.1.0 (2022-09-23) 117 | * (Apollon77) Fix setAway 118 | * (Apollon77) Adjust setAway/setHome message responses to return all errors/responses when multiple calls where done for multiple homes or persons 119 | 120 | ### 2.0.5 (2022-09-16) 121 | * (Apollon77) Catch communication errors better 122 | 123 | ### 2.0.4 (2022-09-15) 124 | * (Apollon77) Fix crash case with Smoke detector events 125 | 126 | ### 2.0.3 (2022-09-14) 127 | * (Apollon77) Fixes and Optimizations for Doorbell devices 128 | 129 | ### 2.0.2 (2022-09-12) 130 | IMPORTANT: This Adapter requires Admin 6.2.14+ to be configured! 131 | * BREAKING: Object structure changes completely and now uses unique IDs instead of names! 132 | * (Apollon77) Change the Authentication method as requested by Netatmo till October 2022 133 | * (Apollon77) Doorbell integration 134 | * (Apollon77) Converted to new APIs, values of several objects might be different 135 | * (Apollon77) Fix crash cases reported by Sentry 136 | * (Apollon77) Adjust setAway to the current API 137 | * (Apollon77) Added setHome function (Welcome only) to mark all or specific persons as home (requires your own API key!) 138 | * (Apollon77) setAway and setHome now also return the result of the call as callback tzo the message 139 | * (Apollon77) Allow to edit floodlight and monitoring-state 140 | 141 | ### 1.7.1 (2022-03-30) 142 | * (Apollon77) Fix Event cleanup 143 | 144 | ### 1.7.0 (2022-03-24) 145 | * IMPORTANT: js-controller 3.3.19 is needed at least! 146 | * (Apollon77) Activate events again (manually delete objects once if you get type errors) 147 | * (Apollon77) Adjust some roles and written data to prevent warnings in logs 148 | 149 | ### 1.6.0 (2022-03-13) 150 | * (Apollon77) Important: In person names (Welcome) in state IDs forbidden characters are now replaces by _!! 151 | * (Apollon77) Fix another potential crash case reported by sentry 152 | 153 | ### 1.5.1 (2022-03-09) 154 | * (Apollon77) Fix jsonconfig for Client secret 155 | 156 | ### 1.5.0 (2022-03-08) 157 | * (kyuka-dom) Added support for netatmo carbon monoxide sensor. 158 | * (kyuka-dom) Added support for netatmo smoke alarm. 159 | * (foxriver76) prevent crashes if application limit reached 160 | * (Apollon77) Allow to specify own id/secret in all cases 161 | * (Apollon77/foxriver76) ensure that minimum polling interval of 10 minutes is respected if no individual ID/Secret is provided 162 | * (Apollon77) Several pother fixes and optimizations 163 | * (Apollon77) Add Sentry for crash reporting 164 | 165 | ### 1.4.4 (2021-07-21) 166 | * (Apollon77) Fix typo that lead to a crash 167 | 168 | ### 1.4.3 (2021-06-27) 169 | * (Apollon77) Fix typo to fix crash 170 | 171 | ### 1.4.2 (2021-06-27) 172 | * (bluefox) Removed warnings about the type of states 173 | 174 | ### 1.4.0 (2021-06-24) 175 | * (bluefox) Added the support of admin5 176 | * (bluefox) Removed warnings about the type of states 177 | 178 | ### 1.3.3 179 | * (PArns) removed person history 180 | 181 | ### 1.3.2 182 | * (PArns) Updated libs & merged pending patches 183 | * (PArns) Changed update interval from 5 to 10 minutes (requested by Netatmo) 184 | 185 | ### 1.3.1 186 | * (PArns) Fixed event cleanup crash 187 | 188 | ### 1.3.0 189 | * (HMeyer) Added Netatmo Coach 190 | 191 | ### 1.2.2 192 | * (PArns) Updated meta info 193 | 194 | ### 1.2.0 195 | * (PArns) Fixed camera picture for events 196 | * (PArns) Added camera vignette for events 197 | * (PArns) Added camera video for events 198 | * (PArns) Added new sub event type (human, vehicle, animal, unknown) 199 | * (PArns) Added LastEventID within the LastEventData section 200 | 201 | ### 1.1.7 202 | * (PArns) Added missing lib dependencies 203 | 204 | ### 1.1.6 205 | * (PArns) Removed GIT requirement and included netatmo lib directly 206 | 207 | ### 1.1.5 208 | * (PArns) Removed 502 error output if API has backend problems 209 | 210 | ### 1.1.4 211 | * (PArns) Added support for unnamed modules 212 | 213 | ### 1.1.1 214 | * (PArns) Simplified setAway 215 | 216 | ### 1.1.0 217 | * (PArns) Added setAway function (Welcome only) to mark all or specific persons as away (requires your own API key!) 218 | 219 | ### 1.0.1 220 | * (PArns) Fixed scope problems for presence & welcome (requires your own API key!) 221 | 222 | ### 1.0.0 223 | * (PArns) Added live camera picture & stream for presence & welcome 224 | * (PArns) Fixed known & unknown face image url for presence & welcome 225 | 226 | ### 0.6.2 227 | * (PArns) Added name of last seen known face 228 | 229 | ### 0.6.1 230 | * (PArns) Changed realtime server to use new general realtime server 231 | * (PArns) Changed enums to channels to avoid enum creation 232 | * (PArns) Simplified detection for movement-, known- & unknown- face events 233 | 234 | ### 0.6.0 235 | * (PArns) Rewritten realtime updates to not need a local server any longer! Realtime updates are now turned on by default if a Welcome or Present cam is available 236 | 237 | ### 0.5.1 238 | * (PArns) Optimized realtime updates to avoid updates if only movement was detected 239 | 240 | ### 0.5.0 241 | * (PArns) Added realtime events for Netatmo Welcome 242 | 243 | ### 0.4.1 244 | * (PArns) Removed log warnings for Wind sensor 245 | 246 | ### 0.4.0 247 | * (PArns) Added absolute humidity 248 | * (PArns) Added dewpoint 249 | 250 | ### 0.3.1 251 | * (PArns) Reuse of preconfigured OAuth Client data 252 | * (PArns) Added backward compatibility with existing installations 253 | 254 | ### 0.3.0 255 | * (wep4you) Initial implementation of Netatmo welcome camera 256 | 257 | ### 0.2.2 258 | * (PArns) Fixed SumRain24MaxDate & SumRain24Max which won't update in some rare cases 259 | 260 | ### 0.2.1 261 | * (PArns) Corrected DateTime values & object types 262 | 263 | ### 0.2.0 264 | * (PArns) Added SumRain1Max/SumRain1MaxDate & SumRain24Max/SumRain24MaxDate to get overall rain max since adapter installation 265 | 266 | ### 0.1.1 267 | * (PArns) Fixed TemperatureAbsoluteMin/TemperatureAbsoluteMax 268 | 269 | ### 0.1.0 270 | * (PArns) Fixed CO2 calibrating status 271 | * (PArns) Added last update for devices 272 | * (PArns) Added TemperatureAbsoluteMin/TemperatureAbsoluteMax to get overall temperature min/max since adapter installation 273 | 274 | ### 0.0.4 275 | * (PArns) Fixed typo/missing parameter in GustStrength 276 | 277 | ### 0.0.3 278 | * (PArns) Added error handling to prevent exceptions for missing parameters 279 | 280 | ### 0.0.2 281 | * (PArns) Fixed rain sensor 282 | 283 | ### 0.0.1 284 | * (PArns) Initial release 285 | 286 | ## License 287 | MIT 288 | 289 | Copyright (c) 2016-2025 Patrick Arns 290 | -------------------------------------------------------------------------------- /lib/netatmoCOSensor.js: -------------------------------------------------------------------------------- 1 | module.exports = function (myapi, myadapter) { 2 | const api = myapi; 3 | const adapter = myadapter; 4 | const cleanUpInterval = adapter.config.cleanup_interval; 5 | const EventTime = adapter.config.event_time ? adapter.config.event_time : 12; 6 | 7 | const eventCleanUpTimer = {}; 8 | 9 | let homeIds = []; 10 | let moduleIds = []; 11 | 12 | let finalized = false; 13 | 14 | let that = null; 15 | 16 | const EventEmitterBridge = require('./eventEmitterBridge.js') 17 | let eeB = null; 18 | 19 | const CODetectorEvents = ['sound_test', 'battery_status', 'wifi_status', 'tampered', 'co_detected']; 20 | const CODetectorSubtypes = { 21 | 0: 'OK', 22 | 1: 'Pre-alarm', 23 | 2: 'Alarm' 24 | }; 25 | const WifiSubtypes = { 26 | 0: 'Failed', 27 | 1: 'OK' 28 | }; 29 | const BatterySubtypes = { 30 | 0: 'LOW', 31 | 1: 'Very LOW' 32 | }; 33 | const SoundSubtypes = { 34 | 0: 'OK', 35 | 1: 'Failed' 36 | }; 37 | const TamperedSubtypes = { 38 | 0: 'OK', 39 | 1: 'Failed' 40 | }; 41 | 42 | this.init = function () { 43 | that = this; 44 | 45 | eeB = new EventEmitterBridge(api, adapter) 46 | adapter.log.info(`CO-Sensor: Registering realtime events with iot instance`); 47 | eeB.on('alert', async data => await onSocketAlert(data)); 48 | }; 49 | 50 | this.finalize = function () { 51 | finalized = true; 52 | if (eeB) { 53 | adapter.log.info('CO-Sensor: Unregistering realtime events'); 54 | eeB.destructor(); 55 | eeB = null; 56 | } 57 | Object.keys(eventCleanUpTimer).forEach(id => clearInterval(eventCleanUpTimer[id])); 58 | }; 59 | 60 | 61 | this.requestUpdateCOSensor = function () { 62 | return new Promise(resolve => { 63 | api.homedataExtended({ 64 | gateway_types: 'NCO' 65 | }, async (err, data) => { 66 | if (finalized) return; 67 | if (err !== null) { 68 | adapter.log.error(err); 69 | } else { 70 | const homes = data.homes; 71 | homeIds = []; 72 | moduleIds = []; 73 | 74 | if (Array.isArray(homes)) { 75 | for (let h = 0; h < homes.length; h++) { 76 | const aHome = homes[h]; 77 | if (!aHome.modules) continue; 78 | adapter.log.debug(`Get CO Sensor for Home ${h}: ${JSON.stringify(aHome)}`); 79 | 80 | await handleHome(aHome); 81 | 82 | //const homeId = aHome.id.replace(/:/g, '-'); // formatName(aHome.name); 83 | //eventCleanUpTimer[homeId] = eventCleanUpTimer[homeId] || setInterval(() => 84 | // cleanUpEvents(homeId), cleanUpInterval * 60 * 1000); 85 | } 86 | } 87 | } 88 | resolve(); 89 | }); 90 | }); 91 | }; 92 | 93 | async function onSocketAlert(data) { 94 | if (!data || (data.device_id && !moduleIds.includes(data.device_id)) || data.event_type === undefined) { 95 | adapter.log.debug(`new alarm (carbon) IGNORE ${JSON.stringify(data)}`); 96 | return; 97 | } 98 | adapter.log.debug(`new alarm (carbon) ${JSON.stringify(data)}`); 99 | 100 | const now = new Date().toString(); 101 | 102 | if (data) { 103 | const path = `${data.home_id.replace(/:/g, '-')}.LastEventData.`; // formatName(data.home_name) + '.LastEventData.'; 104 | const devicePath = `${data.home_id.replace(/:/g, '-')}.${data.device_id.replace(/:/g, '-')}.`; 105 | if (CODetectorEvents.includes(data.event_type)) { 106 | await adapter.setStateAsync(`${path}LastPushType`, {val: data.push_type, ack: true}); 107 | await adapter.setStateAsync(`${path}LastEventType`, {val: data.event_type, ack: true}); 108 | await adapter.setStateAsync(`${path}LastEventDeviceId`, {val: data.device_id, ack: true}); 109 | await adapter.setStateAsync(`${path}LastEventId`, {val: data.event_id, ack: true}); 110 | await adapter.setStateAsync(`${path}LastEvent`, {val: now, ack: true}); 111 | 112 | await adapter.setStateAsync(`${devicePath}${data.event_type}.LastEvent`, { 113 | val: now, 114 | ack: true 115 | }); 116 | await adapter.setStateAsync(`${devicePath}${data.event_type}.LastEventId`, { 117 | val: data.event_id, 118 | ack: true 119 | }); 120 | 121 | if (data.event_type === 'co_detected') { 122 | await adapter.setStateAsync(`${devicePath}${data.event_type}.SubType`, { 123 | val: CODetectorSubtypes[data.sub_type], 124 | ack: true 125 | }); 126 | } else 127 | if (data.event_type === 'wifi_status') { 128 | await adapter.setStateAsync(`${devicePath}${data.event_type}.SubType`, { 129 | val: WifiSubtypes[data.sub_type], 130 | ack: true 131 | }); 132 | } else 133 | if (data.event_type === 'sound_test') { 134 | await adapter.setStateAsync(`${devicePath}${data.event_type}.SubType`, { 135 | val: SoundSubtypes[data.sub_type], 136 | ack: true 137 | }); 138 | } else 139 | if (data.event_type === 'tampered') { 140 | await adapter.setStateAsync(`${devicePath}${data.event_type}.SubType`, { 141 | val: TamperedSubtypes[data.sub_type], 142 | ack: true 143 | }); 144 | } else 145 | if (data.event_type === "battery_status") { 146 | await adapter.setStateAsync(`${devicePath}${data.event_type}.SubType`, { 147 | val: BatterySubtypes[data.sub_type], 148 | ack: true 149 | }); 150 | } 151 | 152 | let active = false 153 | if (data.sub_type > 0) { 154 | active = true 155 | } 156 | await adapter.setStateAsync(`${devicePath}${data.event_type}.active`, { 157 | val: active, 158 | ack: true 159 | }); 160 | } else { 161 | await that.requestUpdateCOSensor(); 162 | } 163 | } 164 | } 165 | 166 | /* 167 | function formatName(aHomeName) { 168 | return aHomeName.replace(/ /g, '-').replace(/---/g, '-').replace(/--/g, '-').replace(adapter.FORBIDDEN_CHARS, '_').replace(/\s|\./g, '_'); 169 | } 170 | */ 171 | 172 | async function handleHome(aHome) { 173 | const homeId = aHome.id.replace(/:/g, '-'); //formatName(aHome.name); 174 | 175 | homeIds.push(aHome.id); 176 | 177 | // Join HomeID 178 | if (eeB) { 179 | eeB.joinHome(aHome.id); 180 | } 181 | 182 | await adapter.extendOrSetObjectNotExistsAsync(homeId, { 183 | type: 'folder', 184 | common: { 185 | name: aHome.name || aHome.id, 186 | }, 187 | native: { 188 | id: aHome.id, 189 | }, 190 | }); 191 | 192 | await adapter.extendOrSetObjectNotExistsAsync(`${homeId}.LastEventData`, { 193 | type: 'channel', 194 | common: { 195 | name: 'LastEventData', 196 | } 197 | }); 198 | 199 | await adapter.extendOrSetObjectNotExistsAsync(`${homeId}.LastEventData.LastPushType`, { 200 | type: 'state', 201 | common: { 202 | name: 'LastPushType', 203 | type: 'string', 204 | role: 'state', 205 | read: true, 206 | write: false 207 | }, 208 | native: { 209 | id: aHome.id 210 | } 211 | }); 212 | 213 | await adapter.extendOrSetObjectNotExistsAsync(`${homeId}.LastEventData.LastEventId`, { 214 | type: 'state', 215 | common: { 216 | name: 'LastEventId', 217 | type: 'string', 218 | role: 'state', 219 | read: true, 220 | write: false 221 | }, 222 | native: { 223 | id: aHome.id 224 | } 225 | }); 226 | 227 | await adapter.extendOrSetObjectNotExistsAsync(`${homeId}.LastEventData.LastEventType`, { 228 | type: 'state', 229 | common: { 230 | name: 'LastEventTypes', 231 | type: 'string', 232 | role: 'state', 233 | read: true, 234 | write: false 235 | }, 236 | native: { 237 | id: aHome.id 238 | } 239 | }); 240 | 241 | await adapter.extendOrSetObjectNotExistsAsync(`${homeId}.LastEventData.LastEventDeviceId`, { 242 | type: 'state', 243 | common: { 244 | name: 'LastEventDeviceId', 245 | type: 'string', 246 | role: 'state', 247 | read: true, 248 | write: false 249 | }, 250 | native: { 251 | id: aHome.id 252 | } 253 | }); 254 | 255 | await adapter.extendOrSetObjectNotExistsAsync(`${homeId}.LastEventData.LastEvent`, { 256 | type: 'state', 257 | common: { 258 | name: 'LastEvent', 259 | type: 'string', 260 | role: 'value.date', 261 | read: true, 262 | write: false 263 | }, 264 | native: { 265 | id: aHome.id 266 | } 267 | }); 268 | 269 | // adapter.log.debug(JSON.stringify(aHome)) 270 | 271 | if (aHome.modules) { 272 | for (const aCODetector of aHome.modules) { 273 | if (aCODetector.id) { 274 | moduleIds.push(aCODetector.id); 275 | await handleCODetector(aCODetector, aHome); 276 | } 277 | } 278 | } 279 | } 280 | 281 | async function handleCODetector(aCODetector, aHome) { 282 | const aParent = aHome.id.replace(/:/g, '-'); // formatName(aHome.name); 283 | const aParentRooms = aHome.rooms; 284 | const fullPath = `${aParent}.${aCODetector.id.replace(/:/g, '-')}`; 285 | const infoPath = `${fullPath}.info`; 286 | 287 | await adapter.extendOrSetObjectNotExistsAsync(fullPath, { 288 | type: 'device', 289 | common: { 290 | name: aCODetector.name || aCODetector.id, 291 | }, 292 | native: { 293 | id: aCODetector.id, 294 | type: aCODetector.type 295 | } 296 | }); 297 | 298 | await adapter.extendOrSetObjectNotExistsAsync(infoPath, { 299 | type: 'channel', 300 | common: { 301 | name: `${aCODetector.name || aCODetector.id} Info`, 302 | }, 303 | native: { 304 | } 305 | }); 306 | 307 | if (aCODetector.id) { 308 | await adapter.extendOrSetObjectNotExistsAsync(`${infoPath}.id`, { 309 | type: 'state', 310 | common: { 311 | name: 'CODetector ID', 312 | type: 'string', 313 | role: 'state', 314 | read: true, 315 | write: false 316 | } 317 | }); 318 | await adapter.setStateAsync(`${infoPath}.id`, {val: aCODetector.id, ack: true}); 319 | } 320 | 321 | 322 | if (aCODetector.setup_date) { 323 | await adapter.extendOrSetObjectNotExistsAsync(`${infoPath}.setup_date`, { 324 | type: 'state', 325 | common: { 326 | name: 'CODetector setup date', 327 | type: 'string', 328 | role: 'state', 329 | read: true, 330 | write: false 331 | } 332 | }); 333 | 334 | await adapter.setStateAsync(`${infoPath}.setup_date`, { 335 | val: new Date(aCODetector.setup_date * 1000).toString(), 336 | ack: true 337 | }); 338 | } 339 | 340 | if (aCODetector.name) { 341 | await adapter.extendOrSetObjectNotExistsAsync(`${infoPath}.name`, { 342 | type: 'state', 343 | common: { 344 | name: 'CODetector name', 345 | type: 'string', 346 | role: 'state', 347 | read: true, 348 | write: false 349 | } 350 | }); 351 | 352 | await adapter.setStateAsync(`${infoPath}.name`, {val: aCODetector.name, ack: true}); 353 | } 354 | 355 | if (aCODetector.room_id) { 356 | const roomName = aParentRooms.find((r) => r.id === aCODetector.room_id) 357 | if (roomName) { 358 | await adapter.extendOrSetObjectNotExistsAsync(`${infoPath}.room`, { 359 | type: 'state', 360 | common: { 361 | name: 'CODetector Room', 362 | type: 'string', 363 | role: 'state', 364 | read: true, 365 | write: false 366 | } 367 | }); 368 | 369 | await adapter.setStateAsync(`${infoPath}.room`, {val: roomName.name, ack: true}); 370 | } 371 | } 372 | 373 | for (const e of CODetectorEvents) { 374 | await adapter.extendOrSetObjectNotExistsAsync(`${fullPath}.${e}`, { 375 | type: 'channel', 376 | common: { 377 | name: e, 378 | } 379 | }); 380 | 381 | await adapter.extendOrSetObjectNotExistsAsync(`${fullPath}.${e}.LastEventId`, { 382 | type: 'state', 383 | common: { 384 | name: 'LastEventId', 385 | type: 'string', 386 | role: 'state', 387 | read: true, 388 | write: false 389 | }, 390 | native: { 391 | id: aHome.id, 392 | event: e 393 | } 394 | }); 395 | 396 | await adapter.extendOrSetObjectNotExistsAsync(`${fullPath}.${e}.LastEvent`, { 397 | type: 'state', 398 | common: { 399 | name: 'LastEvent', 400 | type: 'string', 401 | role: 'value.date', 402 | read: true, 403 | write: false 404 | }, 405 | native: { 406 | id: aHome.id, 407 | event: e 408 | } 409 | }); 410 | 411 | await adapter.extendOrSetObjectNotExistsAsync(`${fullPath}.${e}.SubType`, { 412 | type: 'state', 413 | common: { 414 | name: 'SubType', 415 | type: 'string', 416 | role: 'state', 417 | read: true, 418 | write: false 419 | }, 420 | native: { 421 | id: aHome.id, 422 | event: e 423 | } 424 | }); 425 | 426 | 427 | await adapter.extendOrSetObjectNotExistsAsync(`${fullPath}.${e}.active`, { 428 | type: 'state', 429 | common: { 430 | name: 'active', 431 | type: 'boolean', 432 | role: 'state', 433 | read: true, 434 | write: false 435 | }, 436 | native: { 437 | id: aHome.id, 438 | event: e 439 | } 440 | }); 441 | } 442 | 443 | // Initialize CODetector Place 444 | if (aHome.place) { 445 | await handlePlace(aHome.place, fullPath); 446 | } 447 | } 448 | 449 | async function handlePlace(aPlace, aParent) { 450 | const fullPath = `${aParent}.place`; 451 | 452 | await adapter.extendOrSetObjectNotExistsAsync(fullPath, { 453 | type: 'channel', 454 | common: { 455 | name: 'place', 456 | } 457 | }); 458 | 459 | if (aPlace.city) { 460 | await adapter.extendOrSetObjectNotExistsAsync(`${fullPath}.city`, { 461 | type: 'state', 462 | common: { 463 | name: 'city', 464 | type: 'string', 465 | role: 'state', 466 | read: true, 467 | write: false 468 | } 469 | }); 470 | 471 | await adapter.setStateAsync(`${fullPath}.city`, {val: aPlace.city, ack: true}); 472 | } 473 | 474 | if (aPlace.country) { 475 | await adapter.extendOrSetObjectNotExistsAsync(`${fullPath}.country`, { 476 | type: 'state', 477 | common: { 478 | name: 'country', 479 | type: 'string', 480 | role: 'state', 481 | read: true, 482 | write: false 483 | } 484 | }); 485 | 486 | await adapter.setStateAsync(`${fullPath}.country`, {val: aPlace.country, ack: true}); 487 | } 488 | 489 | if (aPlace.timezone) { 490 | await adapter.extendOrSetObjectNotExistsAsync(`${fullPath}.timezone`, { 491 | type: 'state', 492 | common: { 493 | name: 'timezone', 494 | type: 'string', 495 | role: 'state', 496 | read: true, 497 | write: false 498 | } 499 | }); 500 | 501 | await adapter.setStateAsync(`${fullPath}.timezone`, {val: aPlace.timezone, ack: true}); 502 | } 503 | } 504 | 505 | /* 506 | function cleanUpEvents(home) { 507 | adapter.getForeignObjects(`netatmo.${adapter.instance}.${home}.Events.*`, 'channel', (errEvents, objEvents) => { 508 | if (errEvents) { 509 | adapter.log.error(errEvents); 510 | } else if (objEvents) { 511 | const cleanupDate = new Date().getTime() - EventTime * 60 * 60 * 1000; 512 | 513 | for (const aEventId in objEvents) { 514 | //adapter.getForeignObject(aEventId + '.time', 'state', function (errTime, objTime) { 515 | adapter.getState(`${aEventId}.time`, async (errTime, stateTime) => { 516 | if (errTime) { 517 | adapter.log.error(errTime); 518 | } else if (stateTime) { 519 | let eventDate; 520 | 521 | try { 522 | eventDate = new Date(stateTime.val).getTime(); 523 | } catch (e) { 524 | eventDate = null; 525 | } 526 | 527 | adapter.log.debug(`Cleanup CO Events: Check time for ${aEventId}: (${stateTime.val}) ${cleanupDate} > ${eventDate}`); 528 | if ((cleanupDate > eventDate) || eventDate == null) { 529 | adapter.log.info(`CO Event ${aEventId} expired, so cleanup`); 530 | try { 531 | await adapter.delObjectAsync(aEventId, {recursive: true}); 532 | } catch (err) { 533 | adapter.log.warn(`Could not delete object ${aEventId} during cleanup. Please remove yourself.`); 534 | } 535 | } 536 | } 537 | }); 538 | } 539 | } 540 | }); 541 | } 542 | */ 543 | } 544 | -------------------------------------------------------------------------------- /lib/netatmoSmokedetector.js: -------------------------------------------------------------------------------- 1 | module.exports = function (myapi, myadapter) { 2 | const api = myapi; 3 | const adapter = myadapter; 4 | const cleanUpInterval = adapter.config.cleanup_interval; 5 | const EventTime = adapter.config.event_time ? adapter.config.event_time : 12; 6 | 7 | const eventCleanUpTimer = {}; 8 | let finalized = false; 9 | 10 | let homeIds = []; 11 | let moduleIds = []; 12 | 13 | let that = null; 14 | 15 | const SmokeDetectorSubtypes = { 16 | 0: 'OK', 17 | 1: 'Alarm' 18 | }; 19 | const SoundSubtypes = { 20 | 0: 'OK', 21 | 1: 'Failed' 22 | }; 23 | const TamperedSubtypes = { 24 | 0: 'OK', 25 | 1: 'Failed' 26 | }; 27 | const WifiSubtypes = { 28 | 0: 'Failed', 29 | 1: 'OK' 30 | }; 31 | const BatterySubtypes = { 32 | 0: 'LOW', 33 | 1: 'Very LOW' 34 | }; 35 | const ChamberSubtypes = { 36 | 0: 'Clean', 37 | 1: 'Dustiy' 38 | }; 39 | const HushSubtypes = { 40 | 0: 'Loud', 41 | 1: 'Silent' 42 | }; 43 | 44 | const EventEmitterBridge = require('./eventEmitterBridge.js') 45 | let eeB = null; 46 | 47 | const SmokeDetectorEvents = ['sound_test', 'detection_chamber_status', 'battery_status', 'wifi_status', 'tampered', 'smoke', 'hush'] 48 | 49 | this.init = function () { 50 | that = this; 51 | 52 | eeB = new EventEmitterBridge(api, adapter) 53 | adapter.log.info(`Smoke-Detector: Registering realtime events with iot instance`); 54 | eeB.on('alert', async data => await onSocketAlert(data)); 55 | }; 56 | 57 | this.finalize = function () { 58 | finalized = true; 59 | if (eeB) { 60 | adapter.log.info('Smoke-Detector: Unregistering realtime events'); 61 | eeB.destructor(); 62 | eeB = null; 63 | } 64 | Object.keys(eventCleanUpTimer) 65 | .forEach(id => clearInterval(eventCleanUpTimer[id])); 66 | }; 67 | 68 | this.requestUpdateSmokedetector = function () { 69 | return new Promise(resolve => { 70 | api.homedataExtended({ 71 | gateway_types: 'NSD' 72 | }, async (err, data) => { 73 | if (finalized) return; 74 | if (err !== null) { 75 | adapter.log.error(err); 76 | } else { 77 | const homes = data.homes; 78 | homeIds = []; 79 | moduleIds = []; 80 | 81 | if (Array.isArray(homes)) { 82 | for (let h = 0; h < homes.length; h++) { 83 | const aHome = homes[h]; 84 | if (!aHome.modules) { 85 | continue; 86 | } 87 | adapter.log.debug(`Get Smoke Detectors for Home ${h}: ${JSON.stringify(aHome)}`); 88 | 89 | await handleHome(aHome); 90 | 91 | /* 92 | const homeId = aHome.id.replace(/:/g, '-'); // formatName(aHome.name); 93 | eventCleanUpTimer[homeId] = eventCleanUpTimer[homeId] || setInterval(() => 94 | cleanUpEvents(homeId), cleanUpInterval * 60 * 1000); 95 | */ 96 | } 97 | } 98 | } 99 | resolve(); 100 | }); 101 | }); 102 | }; 103 | 104 | async function onSocketAlert(data) { 105 | if (!data || (data.device_id && !moduleIds.includes(data.device_id)) || data.event_type === undefined) { 106 | adapter.log.debug(`new alarm (smoke) IGNORE ${JSON.stringify(data)}`); 107 | return; 108 | } 109 | adapter.log.debug(`new alarm (smoke) ${JSON.stringify(data)}`); 110 | 111 | const now = new Date().toString(); 112 | 113 | if (data) { 114 | const path = `${data.home_id.replace(/:/g, '-')}.LastEventData.`; // formatName(data.home_name) + '.LastEventData.'; 115 | const devicePath = `${data.home_id.replace(/:/g, '-')}.${data.device_id.replace(/:/g, '-')}.`; 116 | if (SmokeDetectorEvents.includes(data.event_type)) { 117 | await adapter.setStateAsync(`${path}LastPushType`, {val: data.push_type, ack: true}); 118 | await adapter.setStateAsync(`${path}LastEventType`, {val: data.event_type, ack: true}); 119 | await adapter.setStateAsync(`${path}LastEventDeviceId`, {val: data.device_id, ack: true}); 120 | await adapter.setStateAsync(`${path}LastEventId`, {val: data.event_id, ack: true}); 121 | await adapter.setStateAsync(`${path}LastEvent`, {val: now, ack: true}); 122 | 123 | await adapter.setStateAsync(`${devicePath}${data.event_type}.LastEvent`, {val: now, ack: true}); 124 | await adapter.setStateAsync(`${devicePath}${data.event_type}.LastEventId`, {val: data.event_id, ack: true}); 125 | if (data.event_type === 'smoke') { 126 | await adapter.setStateAsync(`${devicePath}${data.event_type}.SubType`, { 127 | val: SmokeDetectorSubtypes[data.sub_type], 128 | ack: true 129 | }); 130 | } else 131 | if (data.event_type === 'wifi_status') { 132 | await adapter.setStateAsync(`${devicePath}${data.event_type}.SubType`, { 133 | val: WifiSubtypes[data.sub_type], 134 | ack: true 135 | }); 136 | } else 137 | if (data.event_type === 'sound_test') { 138 | await adapter.setStateAsync(`${devicePath}${data.event_type}.SubType`, { 139 | val: SoundSubtypes[data.sub_type], 140 | ack: true 141 | }); 142 | } else 143 | if (data.event_type === 'tampered') { 144 | await adapter.setStateAsync(`${devicePath}${data.event_type}.SubType`, { 145 | val: TamperedSubtypes[data.sub_type], 146 | ack: true 147 | }); 148 | } else 149 | if (data.event_type === 'battery_status') { 150 | await adapter.setStateAsync(`${devicePath}${data.event_type}.SubType`, { 151 | val: BatterySubtypes[data.sub_type], 152 | ack: true 153 | }); 154 | } else 155 | if (data.event_type === 'detection_chamber_status') { 156 | await adapter.setStateAsync(`${devicePath}${data.event_type}.SubType`, { 157 | val: ChamberSubtypes[data.sub_type], 158 | ack: true 159 | }); 160 | } else 161 | if (data.event_type === 'hush') { 162 | await adapter.setStateAsync(`${devicePath}${data.event_type}.SubType`, { 163 | val: HushSubtypes[data.sub_type], 164 | ack: true 165 | }); 166 | } 167 | 168 | let active = false; 169 | if (data.sub_type > 0) { 170 | active = true; 171 | } 172 | await adapter.setStateAsync(`${devicePath}${data.event_type}.active`, { 173 | val: active, 174 | ack: true 175 | }); 176 | if (active) { 177 | // reset event after 10 sec 178 | setTimeout( () => 179 | !finalized && adapter.setState(`${devicePath}${data.event_type}.active`, {val: false, ack: true}), 10 * 1000); 180 | } 181 | } else { 182 | that.requestUpdateSmokedetector(); 183 | } 184 | } 185 | } 186 | 187 | /* 188 | function formatName(aHomeName) { 189 | return aHomeName.replace(/ /g, '-').replace(/---/g, '-').replace(/--/g, '-').replace(adapter.FORBIDDEN_CHARS, '_').replace(/\s|\./g, '_'); 190 | } 191 | */ 192 | 193 | async function handleHome(aHome) { 194 | const homeId = aHome.id.replace(/:/g, '-'); //formatName(aHome.name); 195 | const fullPath = homeId; 196 | 197 | homeIds.push(aHome.id); 198 | 199 | // Join HomeID 200 | if (eeB) { 201 | eeB.joinHome(aHome.id); 202 | } 203 | 204 | await adapter.extendOrSetObjectNotExistsAsync(homeId, { 205 | type: 'folder', 206 | common: { 207 | name: aHome.name || aHome.id, 208 | }, 209 | native: { 210 | id: aHome.id 211 | } 212 | }); 213 | 214 | 215 | await adapter.extendOrSetObjectNotExistsAsync(`${homeId}.LastEventData`, { 216 | type: 'channel', 217 | common: { 218 | name: 'LastEventData', 219 | } 220 | }); 221 | 222 | await adapter.extendOrSetObjectNotExistsAsync(`${homeId}.LastEventData.LastPushType`, { 223 | type: 'state', 224 | common: { 225 | name: 'LastPushType', 226 | type: 'string', 227 | role: 'state', 228 | read: true, 229 | write: false 230 | }, 231 | native: { 232 | id: aHome.id 233 | } 234 | }); 235 | 236 | await adapter.extendOrSetObjectNotExistsAsync(`${homeId}.LastEventData.LastEventId`, { 237 | type: 'state', 238 | common: { 239 | name: 'LastEventId', 240 | type: 'string', 241 | role: 'state', 242 | read: true, 243 | write: false 244 | }, 245 | native: { 246 | id: aHome.id 247 | } 248 | }); 249 | 250 | await adapter.extendOrSetObjectNotExistsAsync(`${homeId}.LastEventData.LastEventType`, { 251 | type: 'state', 252 | common: { 253 | name: 'LastEventTypes', 254 | type: 'string', 255 | role: 'state', 256 | read: true, 257 | write: false 258 | }, 259 | native: { 260 | id: aHome.id 261 | } 262 | }); 263 | 264 | await adapter.extendOrSetObjectNotExistsAsync(`${homeId}.LastEventData.LastEventDeviceId`, { 265 | type: 'state', 266 | common: { 267 | name: 'LastEventDeviceId', 268 | type: 'string', 269 | role: 'state', 270 | read: true, 271 | write: false 272 | }, 273 | native: { 274 | id: aHome.id 275 | } 276 | }); 277 | 278 | await adapter.extendOrSetObjectNotExistsAsync(`${homeId}.LastEventData.LastEvent`, { 279 | type: 'state', 280 | common: { 281 | name: 'LastEvent', 282 | type: 'string', 283 | role: 'value.date', 284 | read: true, 285 | write: false 286 | }, 287 | native: { 288 | id: aHome.id 289 | } 290 | }); 291 | 292 | if (aHome.modules) { 293 | for (const aSmokeDetector of aHome.modules) { 294 | if (aSmokeDetector.id) { 295 | moduleIds.push(aSmokeDetector.id); 296 | await handleSmokeDetector(aSmokeDetector, aHome); 297 | } 298 | } 299 | } 300 | } 301 | 302 | async function handleSmokeDetector(aSmokeDetector, aHome) { 303 | const aParent = aHome.id.replace(/:/g, '-'); // formatName(aHome.name); 304 | const fullPath = `${aParent}.${aSmokeDetector.id.replace(/:/g, '-')}`; 305 | const infoPath = `${fullPath}.info`; 306 | 307 | await adapter.extendOrSetObjectNotExistsAsync(fullPath, { 308 | type: 'device', 309 | common: { 310 | name: aSmokeDetector.name || aSmokeDetector.id, 311 | }, 312 | native: { 313 | id: aSmokeDetector.id, 314 | type: aSmokeDetector.type 315 | } 316 | }); 317 | 318 | await adapter.extendOrSetObjectNotExistsAsync(infoPath, { 319 | type: 'channel', 320 | common: { 321 | name: `${aSmokeDetector.name || aSmokeDetector.id} Info`, 322 | }, 323 | native: { 324 | } 325 | }); 326 | 327 | if (aSmokeDetector.id) { 328 | await adapter.extendOrSetObjectNotExistsAsync(`${infoPath}.id`, { 329 | type: 'state', 330 | common: { 331 | name: 'SmokeDetector ID', 332 | type: 'string', 333 | role: 'state', 334 | read: true, 335 | write: false 336 | } 337 | }); 338 | await adapter.setStateAsync(`${infoPath}.id`, {val: aSmokeDetector.id, ack: true}); 339 | } 340 | 341 | if (aSmokeDetector.name) { 342 | await adapter.extendOrSetObjectNotExistsAsync(`${infoPath}.name`, { 343 | type: 'state', 344 | common: { 345 | name: 'SmokeDetector name', 346 | type: 'string', 347 | role: 'state', 348 | read: true, 349 | write: false 350 | } 351 | }); 352 | 353 | await adapter.setStateAsync(`${infoPath}.name`, {val: aSmokeDetector.name, ack: true}); 354 | } 355 | 356 | if (aSmokeDetector.last_setup) { 357 | await adapter.setObjectNotExistsAsync(`${infoPath}.last_setup`, { 358 | type: 'state', 359 | common: { 360 | name: 'SmokeDetector setup date', 361 | type: 'string', 362 | role: 'value.date', 363 | read: true, 364 | write: false 365 | } 366 | }); 367 | 368 | await adapter.setStateAsync(`${infoPath}.last_setup`, { 369 | val: new Date(aSmokeDetector.last_setup * 1000).toString(), 370 | ack: true 371 | }); 372 | } 373 | 374 | for (const e of SmokeDetectorEvents) { 375 | await adapter.extendOrSetObjectNotExistsAsync(`${fullPath}.${e}`, { 376 | type: 'channel', 377 | common: { 378 | name: e, 379 | } 380 | }); 381 | 382 | await adapter.extendOrSetObjectNotExistsAsync(`${fullPath}.${e}.LastEventId`, { 383 | type: 'state', 384 | common: { 385 | name: 'LastEventId', 386 | type: 'string', 387 | role: 'state', 388 | read: true, 389 | write: false 390 | } 391 | }); 392 | 393 | await adapter.extendOrSetObjectNotExistsAsync(`${fullPath}.${e}.LastEvent`, { 394 | type: 'state', 395 | common: { 396 | name: 'LastEvent', 397 | type: 'string', 398 | role: 'value.date', 399 | read: true, 400 | write: false 401 | }, 402 | native: { 403 | id: aHome.id 404 | } 405 | }); 406 | 407 | await adapter.extendOrSetObjectNotExistsAsync(`${fullPath}.${e}.SubType`, { 408 | type: 'state', 409 | common: { 410 | name: 'SubType', 411 | type: 'string', 412 | role: 'state', 413 | read: true, 414 | write: false 415 | }, 416 | native: { 417 | id: aHome.id, 418 | event: e 419 | } 420 | }); 421 | 422 | await adapter.extendOrSetObjectNotExistsAsync(`${fullPath}.${e}.active`, { 423 | type: 'state', 424 | common: { 425 | name: 'active', 426 | type: 'boolean', 427 | role: 'indicator', 428 | read: true, 429 | write: false 430 | }, 431 | native: { 432 | id: aHome.id 433 | } 434 | }); 435 | } 436 | 437 | // Initialize SmokeDetector Place 438 | if (aHome.place) { 439 | await handlePlace(aHome.place, fullPath); 440 | } 441 | } 442 | 443 | async function handlePlace(aPlace, aParent) { 444 | const fullPath = `${aParent}.place`; 445 | 446 | await adapter.extendOrSetObjectNotExistsAsync(fullPath, { 447 | type: 'channel', 448 | common: { 449 | name: 'place', 450 | } 451 | }); 452 | 453 | if (aPlace.city) { 454 | await adapter.extendOrSetObjectNotExistsAsync(`${fullPath}.city`, { 455 | type: 'state', 456 | common: { 457 | name: 'city', 458 | type: 'string', 459 | role: 'state', 460 | read: true, 461 | write: false 462 | } 463 | }); 464 | 465 | await adapter.setStateAsync(`${fullPath}.city`, {val: aPlace.city, ack: true}); 466 | } 467 | 468 | if (aPlace.country) { 469 | await adapter.extendOrSetObjectNotExistsAsync(`${fullPath}.country`, { 470 | type: 'state', 471 | common: { 472 | name: 'country', 473 | type: 'string', 474 | role: 'state', 475 | read: true, 476 | write: false 477 | } 478 | }); 479 | 480 | await adapter.setStateAsync(`${fullPath}.country`, {val: aPlace.country, ack: true}); 481 | } 482 | 483 | if (aPlace.timezone) { 484 | await adapter.extendOrSetObjectNotExistsAsync(`${fullPath}.timezone`, { 485 | type: 'state', 486 | common: { 487 | name: 'timezone', 488 | type: 'string', 489 | role: 'state', 490 | read: true, 491 | write: false 492 | } 493 | }); 494 | 495 | await adapter.setStateAsync(`${fullPath}.timezone`, {val: aPlace.timezone, ack: true}); 496 | } 497 | } 498 | 499 | /* 500 | function cleanUpEvents(home) { 501 | adapter.getForeignObjects(`netatmo.${adapter.instance}.${home}.Events.*`, 'channel', (errEvents, objEvents) => { 502 | if (errEvents) { 503 | adapter.log.error(errEvents); 504 | } else if (objEvents) { 505 | const cleanupDate = new Date().getTime() - EventTime * 60 * 60 * 1000; 506 | 507 | for (const aEventId in objEvents) { 508 | //adapter.getForeignObject(aEventId + '.time', 'state', function (errTime, objTime) { 509 | adapter.getState(`${aEventId}.time`, async (errTime, stateTime) => { 510 | if (errTime) { 511 | adapter.log.error(errTime); 512 | } else if (stateTime) { 513 | let eventDate; 514 | 515 | try { 516 | eventDate = new Date(stateTime.val).getTime(); 517 | } catch(e) { 518 | eventDate = null; 519 | } 520 | 521 | adapter.log.debug(`Cleanup Smoke Events: Check time for ${aEventId}: (${stateTime.val}) ${cleanupDate} > ${eventDate}`); 522 | if ((cleanupDate > eventDate) || eventDate == null) { 523 | adapter.log.info(`Smoke Event ${aEventId} expired, so cleanup`); 524 | try { 525 | await adapter.delObjectAsync(aEventId, {recursive: true}); 526 | } catch (err) { 527 | adapter.log.warn(`Could not delete object ${aEventId} during cleanup. Please remove yourself.`); 528 | } 529 | } 530 | } 531 | }); 532 | } 533 | } 534 | }); 535 | } 536 | */ 537 | } 538 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | /* jshint -W097 */ 2 | /* jshint strict: false */ 3 | /* jslint node: true */ 4 | 'use strict'; 5 | 6 | const adapterName = require('./package.json').name.split('.').pop(); 7 | const utils = require('@iobroker/adapter-core'); 8 | const fs = require('fs'); 9 | 10 | const netatmo = require('./lib/netatmoLib'); 11 | let api = null; 12 | 13 | const NetatmoCoach = require('./lib/netatmoCoach'); 14 | let coach = null; 15 | 16 | const NetatmoStation = require('./lib/netatmoStation'); 17 | let station = null; 18 | 19 | const NetatmoWelcome = require('./lib/netatmoWelcome'); 20 | let welcome = null; 21 | 22 | const NetatmoSmokedetector = require('./lib/netatmoSmokedetector'); 23 | let smokedetector = null; 24 | 25 | const NetatmoCOSensor = require('./lib/netatmoCOSensor'); 26 | let cosensor = null; 27 | 28 | const NetatmoDoorBell = require('./lib/netatmoDoorBell'); 29 | let doorbell = null; 30 | 31 | const NetatmoBubendorff = require('./lib/netatmoBubendorff'); 32 | let bubendorff = null; 33 | 34 | let _coachUpdateInterval; 35 | let _weatherUpdateInterval; 36 | let _welcomeUpdateInterval; 37 | let _smokedetectorUpdateInterval; 38 | let _cosensorUpdateInterval; 39 | let _doorbellUpdateInterval; 40 | let _bubendorffUpdateInterval; 41 | 42 | let usedClientId; 43 | let usedClientSecret; 44 | let usedScopes; 45 | let storedOAuthData = {}; 46 | let dataDir; 47 | let stopped = false; 48 | 49 | const extendedObjects = {}; 50 | 51 | const DEFAULT_CLIENT_ID = '574ddd152baa3cf9598b46cd'; 52 | const DEFAULT_CLIENT_SECRET = '6e3UcBKp005k9N0tpwp69fGYECqOpuhtEE9sWJW'; 53 | 54 | let adapter; 55 | 56 | function startAdapter(options) { 57 | options = options || {}; 58 | Object.assign(options, { 59 | name: adapterName, // adapter name 60 | }); 61 | 62 | adapter = new utils.Adapter(options); 63 | adapter.on('message', obj => { 64 | if (obj.command === 'send') { 65 | obj.command = obj.message; 66 | obj.message = null; 67 | } 68 | 69 | if (obj) { 70 | switch (obj.command) { 71 | case 'setAway': 72 | welcome && welcome.setAway(obj.message, (err, res) => { 73 | obj.callback && adapter.sendTo(obj.from, obj.command, {err, res}, obj.callback); 74 | }); 75 | break; 76 | case 'setHome': 77 | welcome && welcome.setHome(obj.message, (err, res) => { 78 | obj.callback && adapter.sendTo(obj.from, obj.command, {err, res}, obj.callback); 79 | }); 80 | break; 81 | case 'getOAuthStartLink': { 82 | const args = obj.message; 83 | adapter.log.debug(`Received OAuth start message: ${JSON.stringify(args)}`); 84 | args.scope = getScopeList(args.scopes, !!(args.client_id && args.client_secret)); 85 | if (!args.client_id || !args.client_secret) { 86 | if (args.client_id || args.client_secret) { 87 | adapter.log.warn(`Only one of client_id or client_secret was set, using default values!`); 88 | } 89 | args.client_id = DEFAULT_CLIENT_ID; 90 | args.client_secret = DEFAULT_CLIENT_SECRET; 91 | } 92 | if (!args.redirect_uri_base.endsWith('/')) args.redirect_uri_base += '/'; 93 | args.redirect_uri = `${args.redirect_uri_base}oauth2_callbacks/${adapter.namespace}/`; 94 | delete args.redirect_uri_base; 95 | adapter.log.debug(`Get OAuth start link data: ${JSON.stringify(args)}`); 96 | const redirectData = api.getOAuth2AuthenticateStartLink(args); 97 | storedOAuthData[redirectData.state] = args; 98 | 99 | adapter.log.debug(`Get OAuth start link: ${redirectData.url}`); 100 | obj.callback && adapter.sendTo(obj.from, obj.command, {openUrl: redirectData.url}, obj.callback); 101 | break; 102 | } 103 | case 'oauth2Callback': { 104 | const args = obj.message; 105 | adapter.log.debug(`OAuthRedirectReceived: ${JSON.stringify(args)}`); 106 | 107 | if (!args.state || !args.code) { 108 | adapter.log.warn(`Error on OAuth callback: ${JSON.stringify(args)}`); 109 | if (args.error) { 110 | obj.callback && adapter.sendTo(obj.from, obj.command, {error: `Netatmo error: ${args.error}. Please try again.`}, obj.callback); 111 | } else { 112 | obj.callback && adapter.sendTo(obj.from, obj.command, {error: `Netatmo invalid response: ${JSON.stringify(args)}. Please try again.`}, obj.callback); 113 | } 114 | return; 115 | } 116 | 117 | api.authenticate(args, async err => { 118 | if (!err && storedOAuthData[args.state]) { 119 | const storedArgs = storedOAuthData[args.state]; 120 | const native = storedArgs.scopes; 121 | if (api.client_id !== DEFAULT_CLIENT_ID) { 122 | native.id = api.client_id; 123 | native.secret = api.client_secret; 124 | } 125 | native.username = null; 126 | native.password = null; 127 | 128 | const tokenData = { 129 | access_token: api.access_token, 130 | refresh_token: api.refresh_token, 131 | scope: api.scope, 132 | client_id: api.client_id 133 | } 134 | try { 135 | adapter.log.info(`Save OAuth data: ${JSON.stringify(tokenData)}`); 136 | fs.writeFileSync(`${dataDir}/tokens.json`, JSON.stringify(tokenData), 'utf8'); 137 | } catch (err) { 138 | adapter.log.error(`Cannot write token file: ${err}`); 139 | } 140 | 141 | obj.callback && adapter.sendTo(obj.from, obj.command, {result: 'Tokens updated successfully.'}, obj.callback); 142 | 143 | adapter.log.info('Update data in adapter configuration ... restarting ...'); 144 | adapter.extendForeignObject(`system.adapter.${adapter.namespace}`, { 145 | native 146 | }); 147 | } else { 148 | adapter.log.error(`OAuthRedirectReceived: ${err}`); 149 | obj.callback && adapter.sendTo(obj.from, obj.command, {error: `Error getting new tokens from Netatmo: ${err}. Please try again.`}, obj.callback); 150 | } 151 | }); 152 | 153 | break; 154 | } 155 | default: 156 | adapter.log.warn(`Unknown command: ${obj.command}`); 157 | break; 158 | } 159 | } 160 | 161 | return true; 162 | }); 163 | 164 | adapter.on('unload', callback => { 165 | try { 166 | stopped = true; 167 | cleanupResources(); 168 | adapter.log.info('cleaned everything up...'); 169 | callback(); 170 | } catch (e) { 171 | callback(); 172 | } 173 | }); 174 | 175 | adapter.on('stateChange', (id, state) => { 176 | adapter.log.debug(`stateChange ${id} ${JSON.stringify(state)}`); 177 | if (state && !state.ack) { 178 | if (id.startsWith(adapter.namespace)) { 179 | id = id.substring(adapter.namespace.length + 1); 180 | } 181 | if (extendedObjects[id] && extendedObjects[id].native && extendedObjects[id].native.homeId) { 182 | const obj = extendedObjects[id]; 183 | adapter.log.debug(`set state for field ${obj.native.field}`); 184 | api.setState( 185 | obj.native.homeId, 186 | obj.native.moduleId, 187 | obj.native.field, 188 | obj.native.setValue !== undefined ? obj.native.setValue : state.val, 189 | obj.native.bridgeId, 190 | (err, res) => { 191 | if (err) { 192 | adapter.log.error(`Cannot set state ${id}: ${err}`); 193 | } else { 194 | adapter.log.debug(`State ${id} set successfully`); 195 | // update data if set was successful 196 | welcome && welcome.situativeUpdate(obj.native.homeId, obj.native.moduleId); 197 | bubendorff && bubendorff.situativeUpdate(obj.native.homeId, obj.native.moduleId); 198 | } 199 | }); 200 | } 201 | } 202 | }); 203 | 204 | adapter.on('ready', () => main()); 205 | } 206 | 207 | function cleanupResources() { 208 | try { 209 | _coachUpdateInterval && clearInterval(_coachUpdateInterval); 210 | _weatherUpdateInterval && clearInterval(_weatherUpdateInterval); 211 | _welcomeUpdateInterval && clearInterval(_welcomeUpdateInterval); 212 | _smokedetectorUpdateInterval && clearInterval(_smokedetectorUpdateInterval); 213 | _cosensorUpdateInterval && clearInterval(_cosensorUpdateInterval); 214 | _doorbellUpdateInterval && clearInterval(_doorbellUpdateInterval); 215 | _bubendorffUpdateInterval && clearInterval(_bubendorffUpdateInterval); 216 | 217 | station && station.finalize(); 218 | coach && coach.finalize(); 219 | welcome && welcome.finalize(); 220 | smokedetector && smokedetector.finalize(); 221 | cosensor && cosensor.finalize(); 222 | doorbell && doorbell.finalize(); 223 | bubendorff && bubendorff.finalize(); 224 | } catch (err) { 225 | // ignore 226 | } 227 | } 228 | 229 | function getScopeList(scopes, individualCredentials) { 230 | let scope = ''; 231 | 232 | if (scopes.netatmoCoach) { 233 | scope += ' read_homecoach'; 234 | } 235 | 236 | if (scopes.netatmoWelcome) { 237 | scope += ' read_camera read_presence'; 238 | 239 | if (individualCredentials) { 240 | scope += ' access_camera access_presence write_camera write_presence' 241 | } else { 242 | adapter.log.info(`Welcome & Presence support limited because no individual ID/Secret provided.`); 243 | } 244 | } 245 | 246 | if (scopes.netatmoSmokedetector) { 247 | if (individualCredentials) { 248 | scope += ' read_smokedetector'; 249 | } else { 250 | adapter.log.warn(`Smoke detector only supported with individual ID/Secret. Disabling!`); 251 | scopes.netatmoSmokedetector = false; 252 | } 253 | } 254 | 255 | if (scopes.netatmoCOSensor) { 256 | if (individualCredentials) { 257 | scope += ' read_carbonmonoxidedetector'; 258 | } else { 259 | adapter.log.warn(`CO sensor only supported with individual ID/Secret. Disabling!`); 260 | scopes.netatmoCOSensor = false; 261 | } 262 | } 263 | 264 | if (scopes.netatmoDoorBell) { 265 | if (individualCredentials) { 266 | scope += ' read_doorbell access_doorbell'; 267 | } else { 268 | adapter.log.warn(`Doorbell only supported with individual ID/Secret. Disabling!`); 269 | scopes.netatmoDoorBell = false; 270 | } 271 | } 272 | 273 | if (scopes.netatmoBubendorff) { 274 | scope += ' read_bubendorff write_bubendorff'; 275 | } 276 | 277 | // If nothing is set, activate at least the Weatherstation 278 | if (!(scopes.netatmoCoach || scopes.netatmoWeather || scopes.netatmoWelcome || scopes.netatmoSmokedetector || scopes.netatmoCOSensor || scopes.netatmoDoorBell || scopes.netatmoBubendorff)) { 279 | adapter.log.info('No product was chosen, using Weather station as default!'); 280 | scopes.netatmoWeather = true; 281 | } 282 | 283 | if (scopes.netatmoWeather) { 284 | scope += ' read_station'; 285 | } 286 | 287 | scope = scope.trim(); 288 | 289 | return scope; 290 | } 291 | 292 | function isEquivalent(a, b) { 293 | //adapter.log.debug('Compare ' + JSON.stringify(a) + ' with ' + JSON.stringify(b)); 294 | // Create arrays of property names 295 | if (a === null || a === undefined || b === null || b === undefined) { 296 | return (a === b); 297 | } 298 | const aProps = Object.getOwnPropertyNames(a); 299 | const bProps = Object.getOwnPropertyNames(b); 300 | 301 | // If number of properties is different, 302 | // objects are not equivalent 303 | if (aProps.length !== bProps.length) { 304 | //console.log('num props different: ' + JSON.stringify(aProps) + ' / ' + JSON.stringify(bProps)); 305 | return false; 306 | } 307 | 308 | for (let i = 0; i < aProps.length; i++) { 309 | const propName = aProps[i]; 310 | 311 | if (typeof a[propName] !== typeof b[propName]) { 312 | //console.log('type props ' + propName + ' different'); 313 | return false; 314 | } 315 | if (typeof a[propName] === 'object') { 316 | if (!isEquivalent(a[propName], b[propName])) { 317 | return false; 318 | } 319 | } 320 | else { 321 | // If values of same property are not equal, 322 | // objects are not equivalent 323 | if (a[propName] !== b[propName]) { 324 | //console.log('props ' + propName + ' different'); 325 | return false; 326 | } 327 | } 328 | } 329 | 330 | // If we made it this far, objects 331 | // are considered equivalent 332 | return true; 333 | } 334 | 335 | 336 | function main() { 337 | let scope = ''; 338 | let id = DEFAULT_CLIENT_ID; 339 | let secret = DEFAULT_CLIENT_SECRET; 340 | let individualCredentials = false; 341 | let access_token; 342 | let refresh_token; 343 | 344 | adapter.extendOrSetObjectNotExistsAsync = async (id, obj, options) => { 345 | if (!extendedObjects[id]) { 346 | adapter.log.debug(`Initially Check/Extend object ${id} ...`); 347 | extendedObjects[id] = JSON.parse(JSON.stringify(obj)); 348 | return adapter.extendObjectAsync(id, obj, options); 349 | } else { 350 | if (!isEquivalent(extendedObjects[id], obj)) { 351 | adapter.log.debug(`Update object ${id} ...${JSON.stringify(extendedObjects[id])} => ${JSON.stringify(obj)}`); 352 | extendedObjects[id] = JSON.parse(JSON.stringify(obj)); 353 | return adapter.extendObjectAsync(id, obj, options); 354 | } 355 | } 356 | } 357 | 358 | if (adapter.config.id && adapter.config.secret) { 359 | id = adapter.config.id; 360 | secret = adapter.config.secret; 361 | individualCredentials = true; 362 | adapter.log.debug(`Use individual ID/Secret`); 363 | } 364 | 365 | scope = getScopeList(adapter.config, individualCredentials); 366 | 367 | dataDir = utils.getAbsoluteInstanceDataDir(adapter); 368 | 369 | try { 370 | if (!fs.existsSync(dataDir)) { 371 | fs.mkdirSync(dataDir); 372 | } 373 | if (fs.existsSync(`${dataDir}/tokens.json`)) { 374 | const tokens = JSON.parse(fs.readFileSync(`${dataDir}/tokens.json`, 'utf8')); 375 | if (tokens.client_id !== id) { 376 | adapter.log.info(`Stored tokens belong to the different client ID ${tokens.client_id} and not to the configured ID ... deleting`); 377 | fs.unlinkSync(`${dataDir}/tokens.json`); 378 | } else { 379 | access_token = tokens.access_token; 380 | refresh_token = tokens.refresh_token; 381 | adapter.log.info(`Using stored tokens to initialize ... ${JSON.stringify(tokens)}`); 382 | } 383 | if (tokens.scope !== scope) { 384 | adapter.log.info(`Stored tokens have different scope ${tokens.scope} and not the configured scope ${scope} ... If you miss data please authenticate again!`); 385 | } 386 | } 387 | } catch (err) { 388 | adapter.log.error(`Error reading stored tokens: ${err.message}`); 389 | } 390 | 391 | adapter.config.check_interval = parseInt(adapter.config.check_interval, 10); 392 | adapter.config.cleanup_interval = parseInt(adapter.config.cleanup_interval, 10); 393 | 394 | // we do not allow intervals below 5 minutes 395 | if (!individualCredentials && (isNaN(adapter.config.check_interval) || adapter.config.check_interval < 10)) { 396 | adapter.log.warn(`Invalid check interval "${adapter.config.check_interval}", fallback to 10 minutes`); 397 | adapter.config.check_interval = 10; 398 | } 399 | 400 | if (!individualCredentials && (isNaN(adapter.config.cleanup_interval) || adapter.config.cleanup_interval < 20)) { 401 | adapter.log.warn(`Invalid cleanup interval "${adapter.config.cleanup_interval}", fallback to 60 minutes`); 402 | adapter.config.cleanup_interval = 60; 403 | } 404 | 405 | adapter.config.unknown_person_time = adapter.config.unknown_person_time || 24; 406 | 407 | adapter.config.location_elevation = adapter.config.location_elevation || 0; 408 | 409 | usedClientId = id; 410 | usedClientSecret = secret; 411 | usedScopes = scope; 412 | 413 | const auth = { 414 | 'client_id': id, 415 | 'client_secret': secret, 416 | 'scope': scope, 417 | 'username': adapter.config.username, 418 | 'password': adapter.config.password 419 | }; 420 | if (refresh_token) { 421 | auth.access_token = access_token; 422 | auth.refresh_token = refresh_token; 423 | } 424 | 425 | api = new netatmo(); 426 | api.setAdapter(adapter); 427 | 428 | api.on('error', err => { 429 | adapter.log.warn(`API Error: ${err.message}`); 430 | }); 431 | api.on('warning', err => { 432 | adapter.log.info(`API Warning: ${err.message}`); 433 | }); 434 | api.on('access_token', access_token => { 435 | adapter.log.debug(`Access Token: ${access_token}`); 436 | }); 437 | api.on('refresh_token', refresh_token => { 438 | adapter.log.debug(`Update Refresh tokens: ${refresh_token}`); 439 | const tokenData = { 440 | access_token: api.access_token, 441 | refresh_token: api.refresh_token, 442 | scope: api.scope, 443 | client_id: api.client_id 444 | } 445 | try { 446 | fs.writeFileSync(`${dataDir}/tokens.json`, JSON.stringify(tokenData), 'utf8'); 447 | } catch (err) { 448 | adapter.log.error(`Cannot write token file: ${err}`); 449 | } 450 | }); 451 | api.on('authenticated', () => { 452 | if (stopped) { 453 | return; 454 | } 455 | adapter.log.info(`Successfully authenticated with Netatmo ${api.client_id === DEFAULT_CLIENT_ID ? 'with general ioBroker client' : `with individual client-ID ${api.client_id}`}`); 456 | 457 | cleanupResources(); 458 | initialize(); 459 | adapter.subscribeStates('*'); 460 | }); 461 | 462 | adapter.log.info(`Authenticating with Netatmo ${auth.client_id === DEFAULT_CLIENT_ID ? 'using general ioBroker client' : `using individual client-ID ${auth.client_id}`}`); 463 | try { 464 | api.authenticate(auth); 465 | } catch (err) { 466 | adapter.log.error(`Error while authenticating: ${err.message}`); 467 | } 468 | } 469 | 470 | function initialize() { 471 | if (adapter.config.netatmoCoach) { 472 | coach = new NetatmoCoach(api, adapter); 473 | 474 | coach.requestUpdateCoachStation(); 475 | 476 | _coachUpdateInterval = setInterval(() => 477 | coach.requestUpdateCoachStation(), adapter.config.check_interval * 60 * 1000); 478 | } 479 | 480 | if (adapter.config.netatmoWeather) { 481 | station = new NetatmoStation(api, adapter); 482 | 483 | station.requestUpdateWeatherStation(); 484 | 485 | _weatherUpdateInterval = setInterval(() => 486 | station.requestUpdateWeatherStation(), adapter.config.check_interval * 60 * 1000); 487 | } 488 | 489 | if (adapter.config.netatmoWelcome) { 490 | welcome = new NetatmoWelcome(api, adapter); 491 | welcome.init(); 492 | welcome.requestUpdateIndoorCamera(); 493 | 494 | _welcomeUpdateInterval = setInterval(() => 495 | welcome.requestUpdateIndoorCamera(), adapter.config.check_interval * 2 * 60 * 1000); 496 | } 497 | 498 | if (adapter.config.netatmoSmokedetector) { 499 | smokedetector = new NetatmoSmokedetector(api, adapter); 500 | smokedetector.init(); 501 | smokedetector.requestUpdateSmokedetector(); 502 | 503 | _smokedetectorUpdateInterval = setInterval(() => 504 | smokedetector.requestUpdateSmokedetector(), adapter.config.check_interval * 2 * 60 * 1000); 505 | } 506 | 507 | if (adapter.config.netatmoCOSensor) { 508 | cosensor = new NetatmoCOSensor(api, adapter); 509 | cosensor.init(); 510 | cosensor.requestUpdateCOSensor(); 511 | 512 | _cosensorUpdateInterval = setInterval(() => 513 | cosensor.requestUpdateCOSensor(), adapter.config.check_interval * 2 * 60 * 1000); 514 | } 515 | 516 | if (adapter.config.netatmoDoorBell) { 517 | doorbell = new NetatmoDoorBell(api, adapter); 518 | doorbell.init(); 519 | doorbell.requestUpdateDoorBell(); 520 | 521 | _doorbellUpdateInterval = setInterval(() => 522 | doorbell.requestUpdateDoorBell(), adapter.config.check_interval * 2 * 60 * 1000); 523 | } 524 | 525 | if (adapter.config.netatmoBubendorff) { 526 | bubendorff = new NetatmoBubendorff(api, adapter); 527 | bubendorff.init(); 528 | bubendorff.requestUpdateBubendorff(); 529 | 530 | _bubendorffUpdateInterval = setInterval(() => 531 | bubendorff.requestUpdateBubendorff(), adapter.config.check_interval * 2 * 60 * 1000); 532 | } 533 | } 534 | 535 | // If started as allInOne mode => return function to create instance 536 | if (require.main === module) { 537 | startAdapter(); 538 | } else { 539 | // compact mode 540 | module.exports = startAdapter; 541 | } 542 | 543 | -------------------------------------------------------------------------------- /io-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "common": { 3 | "name": "netatmo", 4 | "version": "3.1.0", 5 | "title": "Netatmo", 6 | "titleLang": { 7 | "en": "Netatmo", 8 | "de": "Netatmo", 9 | "ru": "Нетатмо", 10 | "pt": "Netatmo", 11 | "nl": "Netatmo", 12 | "fr": "Netatmo", 13 | "it": "Nettmo", 14 | "es": "Netatmo", 15 | "pl": "Netatmo", 16 | "zh-cn": "内塔莫", 17 | "uk": "Netatmo" 18 | }, 19 | "news": { 20 | "3.1.0": { 21 | "en": "Add support for Bubendorff roller shutters\nFix Monitoring State for Welcomes\nAllow to just use CO2/Smoke sensors\nOptimize Shutdown procedure", 22 | "de": "Unterstützung für Bubendorff Rolladen hinzufügen\nBehoben von Überwachungsstatus für Willkommen\nLassen Sie einfach CO2/Smoke Sensoren verwenden\nOptimieren Sie den Abschaltvorgang", 23 | "ru": "Добавить поддержку затворов Bubendorff\nFix мониторинг состояния для Welcomes\nПозвольте просто использовать CO2/Smoke датчики\nОптимизация процедуры Shutdown", 24 | "pt": "Adicionar suporte para persianas de rolo Bubendorff\nFix Monitoring State for Welcomes\nPermitir apenas usar sensores CO2/Smoke\nOtimizar procedimento de desligamento", 25 | "nl": "Steun voor Bubendorff rollers\nVertaling:\nSta toe om CO2/Smoke sensoren te gebruiken\nVertaling:", 26 | "fr": "Ajouter le support pour les volets roulants Bubendorff\nÉtat de surveillance fixe pour les bienvenues\nPermettre d'utiliser des capteurs CO2/Smoke\nOptimiser la procédure de fermeture", 27 | "it": "Aggiungi supporto per tapparelle Bubendorff\nFissare lo Stato di Monitoraggio per i Beni\nPermettere di usare solo i sensori CO2/Smoke\nOttimizzare la procedura di spegnimento", 28 | "es": "Añadir soporte para persianas de rodillo Bubendorff\nFix Monitoring State for Welcomes\nPermitir utilizar sensores CO2/Smoke\nOptimize Shutdown procedure", 29 | "pl": "Wspieranie kolejki Bubendorff\nOficjalna strona Welcomes\nAllow korzysta z sensorów CO2/Smoke\nOpis procedury Shutdown", 30 | "uk": "Додайте підтримку валиків Bubendorff\nСтатус на сервери\nДозволяє просто використовувати датчики CO2 / Smoke\nОптимізуйте процедуру відключення", 31 | "zh-cn": "增加布登德雷·雷蒙尔的穿越者\n九、导 言\n仅允许使用CO2/Smoke传感器\n精简舒适程序" 32 | }, 33 | "3.0.0": { 34 | "en": "BREAKING CHANGE: Restructure Realtime events to be received via iot instance (iot >= 1.14.0 required)", 35 | "de": "BREAKING CHANGE: Restructure Realtime-Ereignisse, die über iot-Instanz empfangen werden (iot >= 1,14,0 erforderlich)", 36 | "ru": "ЗАВТРАК ИЗМЕНЕНИЯ: Реструктуризация событий в режиме реального времени, которые должны быть получены через iot экземпляр (iot >= 1.14.0 требуется)", 37 | "pt": "CHANGE DE BREAKING: Reestruturar Eventos em tempo real a serem recebidos via iot instance (iot >= 1.14.0 obrigatório)", 38 | "nl": "Restructure Retime evenementen worden ontvangen via iot instance 01:14,0 vereist", 39 | "fr": "CHANGEMENT DE BREAKING: Restructurer les événements en temps réel à recevoir via l'instance iot (iot ACIA= 1.14.0 requis)", 40 | "it": "PRESTAZIONI: ristrutturazione eventi in tempo reale da ricevere tramite istanza iot (iot >= 1.14.0 richiesto)", 41 | "es": "CAMBIO DE BREAKING: Reestructurar eventos en tiempo real para ser recibidos por instancia iot (iot ю= 1.14.0 requerido)", 42 | "pl": "BREAKING CHANGE: Restructure Realtime (ang.)", 43 | "uk": "BREAKING CHANGE: Реструктуризація подій в режимі реального часу, щоб отримати через iot екземпляр (iot >= 1.14.0 потрібно)", 44 | "zh-cn": "BAKREANGE:通过iot 例如(iot >=1.14.0 要求)接收的结构实时活动" 45 | }, 46 | "2.1.2": { 47 | "en": "Added missing objects for `Welcome` devices", 48 | "de": "Fehlende Objekte für Welcome-Geräte hinzugefügt", 49 | "ru": "Добавлены недостающие объекты для Welcome устройств", 50 | "pt": "Adicionados objetos em falta para dispositivos de boas-vindas", 51 | "nl": "Vermiste objecten toegevoegd voor welkome apparaten", 52 | "fr": "Ajout d'objets manquants pour les dispositifs de bienvenue", 53 | "it": "Aggiunti gli oggetti mancanti per i dispositivi Welcome", 54 | "es": "Añadido objetos perdidos para dispositivos de bienvenida", 55 | "pl": "Zamknięte przedmioty dla urządzeń Welcome", 56 | "uk": "Додано відсутні об'єкти для вітальних пристроїв", 57 | "zh-cn": "添加用于湿度装置的缺失物品" 58 | }, 59 | "2.1.1": { 60 | "en": "Make sure device types that require custom credentials are not selectable in UI without entering them\nFix a potential crash case", 61 | "de": "Stellen Sie sicher, dass Gerätetypen, die benutzerdefinierte Anmeldeinformationen benötigen, in UI nicht wählbar sind, ohne sie einzugeben\nEin potenzieller Crashfall beheben", 62 | "ru": "Убедитесь, что типы устройств, которые требуют пользовательские учетные данные не выбираются в UI, не входя в них\nИсправление потенциального случай аварии", 63 | "pt": "Certifique-se de que os tipos de dispositivo que exigem credenciais personalizadas não são selecionáveis na interface do usuário sem entrar neles\nCorrigir um caso de acidente potencial", 64 | "nl": "Zorg ervoor dat apparaattypes die gebruikelijke referenties nodig hebben niet geselecteerd in UI zonder ze binnen te komen\nMaak een mogelijke crash zaak", 65 | "fr": "Assurez-vous que les types de périphériques qui nécessitent des identifiants personnalisés ne sont pas sélectionnables dans l'interface utilisateur sans les entrer\nCorrection d'un cas d'accident potentiel", 66 | "it": "Assicurarsi che i tipi di dispositivo che richiedono credenziali personalizzate non siano selezionabili in UI senza immetterli\nFissare un potenziale caso di crash", 67 | "es": "Asegúrese de que los tipos de dispositivo que requieren credenciales personalizadas no son seleccionables en UI sin entrar en ellos\nArreglar un caso de accidente potencial", 68 | "pl": "Umożliwia to, że pewne typy urządzeń wymagające konkretnych kredentialów nie są wybierane w interfejsie użytkownika bez wchodzenia do nich\nFix – przypadek katastrofy potencjalnej", 69 | "zh-cn": "确定需要定有证书的设备类型在UI不能选择,而不准他们进入。\n确定潜在事故", 70 | "uk": "Переконайтеся, що типи пристроїв, для яких потрібні спеціальні облікові дані, не можна вибрати в інтерфейсі користувача, не ввівши їх\nВиправте потенційний випадок збою" 71 | }, 72 | "2.1.0": { 73 | "en": "Fix setAway\nAdjust setAway/setHome message responses to return all errors/responses when multiple calls where done for multiple homes or persons", 74 | "de": "Fix Weg\nEinstellen der Einstellungen Home-Nachrichtantworten, um alle Fehler / Antworten zurückzugeben, wenn mehrere Anrufe, wo für mehrere Häuser oder Personen getan", 75 | "ru": "Fix набор Вдали\nНастройка setAway/set Ответы на домашнее сообщение, чтобы вернуть все ошибки/ответы, когда несколько звонков, где это сделано для нескольких домов или людей", 76 | "pt": "Conjunto de fixação A caminho\nAjuste setAway/set Respostas de mensagem inicial para retornar todos os erros/respostas quando várias chamadas feitas para várias casas ou pessoas", 77 | "nl": "Fix set Weg\nVertaling: Thuisberichten reageren om alle fouten en verantwoordelijkheden terug te brengen wanneer meerdere gesprekken plaatsvinden voor meerdere huizen of personen", 78 | "fr": "Correction Away\nRéglage Accueil Messages réponses pour retourner toutes les erreurs/réponses lorsque plusieurs appels sont faits pour plusieurs maisons ou personnes", 79 | "it": "Set di fissaggio Via\nRegolare il setAway/set Risposte del messaggio di casa per restituire tutti gli errori / risposte quando più chiamate dove fatto per più case o persone", 80 | "es": "Juego fijo Away\nAjuste ajustadoAway/set Home message responses to return all errors/responses when multiple calls where done for multiple homes or persons", 81 | "pl": "Fix set Away\nAdjust setAway (ang.). Odpowiedź komunikatu na powrót wszystkich błędów i odpowiedzi w przypadku wielokrotnych połączeń, w których przeprowadzano wiele domów lub osób", 82 | "zh-cn": "九. 固定装置 A. 公路\n调整 当多家住宅或个人要求多家住房或多人做多家事时,家庭对返回所有错误/反应作出反应", 83 | "uk": "Виправити setAway\nНалаштуйте відповіді на повідомлення setAway/setHome, щоб повертати всі помилки/відповіді під час кількох дзвінків для кількох будинків або людей" 84 | }, 85 | "2.0.5": { 86 | "en": "Catch communication errors better", 87 | "de": "Kommunikationsfehler besser einfangen", 88 | "ru": "Поймать ошибки общения лучше", 89 | "pt": "Apanhar erros de comunicação melhor", 90 | "nl": "Vertaling:", 91 | "fr": "Attrapez les erreurs de communication mieux", 92 | "it": "Cattura errori di comunicazione migliore", 93 | "es": "Errores de comunicación de captura mejor", 94 | "pl": "Błędy komunikacji", 95 | "zh-cn": "失误", 96 | "uk": "Краще вловлюйте помилки спілкування" 97 | }, 98 | "2.0.4": { 99 | "en": "Fix crash case with Smoke detector events", 100 | "de": "Crashfall mit Smoke-Detektorereignissen beheben", 101 | "ru": "Исправление случай аварии с событиями детектора дыма", 102 | "pt": "Corrigir caso de acidente com eventos de detector de fumaça", 103 | "nl": "Vertaling:", 104 | "fr": "Boîtier de crash fixe avec des événements de détecteur de fumée", 105 | "it": "Fix caso crash con gli eventi del rilevatore di fumo", 106 | "es": "Caso de choque fijo con eventos de detector de humo", 107 | "pl": "W przypadku katastrofy Fix z wykrywcami Smoke’a", 108 | "zh-cn": "Smoke探测器事故", 109 | "uk": "Виправте випадки збою з подіями детектора диму" 110 | } 111 | }, 112 | "desc": { 113 | "en": "Netatmo", 114 | "de": "Netatmo", 115 | "ru": "Нетатмо", 116 | "pt": "Netatmo", 117 | "nl": "Netatmo", 118 | "fr": "Netatmo", 119 | "it": "Nettmo", 120 | "es": "Netatmo", 121 | "pl": "Netatmo", 122 | "zh-cn": "内塔莫", 123 | "uk": "Netatmo" 124 | }, 125 | "platform": "Javascript/Node.js", 126 | "mode": "daemon", 127 | "icon": "netatmo.png", 128 | "compact": true, 129 | "connectionType": "cloud", 130 | "dataSource": "poll", 131 | "tier": 2, 132 | "enabled": true, 133 | "messagebox": true, 134 | "adminUI": { 135 | "config": "json" 136 | }, 137 | "extIcon": "https://raw.githubusercontent.com/PArns/ioBroker.netatmo/master/admin/netatmo.png", 138 | "keywords": [ 139 | "netatmo" 140 | ], 141 | "readme": "https://github.com/PArns/ioBroker.netatmo/blob/master/README.md", 142 | "loglevel": "info", 143 | "type": "weather", 144 | "authors": [ 145 | "Patrick Arns " 146 | ], 147 | "dependencies": [ 148 | { 149 | "js-controller": ">=6.0.0" 150 | } 151 | ], 152 | "globalDependencies": [ 153 | { 154 | "admin": ">=6.2.13" 155 | } 156 | ], 157 | "plugins": { 158 | "sentry": { 159 | "dsn": "https://2e16cd8e23654a428905f0066a1a6569@sentry.iobroker.net/173" 160 | } 161 | }, 162 | "messages": [ 163 | { 164 | "condition": { 165 | "operand": "and", 166 | "rules": [ 167 | "oldVersion<2.0.0", 168 | "newVersion>=2.0.0" 169 | ] 170 | }, 171 | "title": { 172 | "en": "Object structure changes!", 173 | "de": "Objektstruktur ändert sich!", 174 | "ru": "Изменения структуры объекта!", 175 | "pt": "Mudanças de estrutura de objetos!", 176 | "nl": "Object structuur verandert!", 177 | "fr": "La structure des objets change !", 178 | "it": "La struttura degli oggetti cambia!", 179 | "es": "¡La estructura de objetos cambia!", 180 | "pl": "Zmiany obiektów!", 181 | "zh-cn": "目标结构变化!", 182 | "uk": "Змінюється структура об'єкта!" 183 | }, 184 | "text": { 185 | "en": "Because of needed API changes and also because of the availability of more and more Netatmo devices we decided to change the object structure. Starting with version 2.0 the unique IDs are used instead of the device names. Because of this the object structure will change completely. Please delete the current objects ideally before the upgrade. Additionally the authentication with Netatmo was changed because the former used way will be disabled in October 2022. Older versions of the adapter might not work any longer.", 186 | "de": "Aufgrund von erforderlichen API-Änderungen und auch aufgrund der Verfügbarkeit von mehr und mehr Netatmo-Geräten haben wir uns entschlossen, die Objektstruktur zu ändern. Ab Version 2.0 werden die eindeutigen IDs anstelle der Gerätenamen verwendet. Dadurch ändert sich die Objektstruktur vollständig. Die aktuellen Objekte sollten idealerweise vor dem Upgrade gelöscht werden. Zusätzlich wurde die Authentifizierung mit Netatmo geändert, da der frühere Gebrauchsweg im Oktober 2022 deaktiviert wird. Ältere Versionen des Adapters funktionieren möglicherweise nicht mehr.", 187 | "ru": "Из-за необходимых изменений API, а также из-за наличия более и более устройств Netatmo мы решили изменить структуру объекта. Начиная с версии 2.0 уникальные идентификаторы используются вместо названия устройств. Из-за этой структуры объекта полностью изменится. Пожалуйста, удалите текущие объекты в идеале перед обновлением. Дополнительно была изменена аутентификация с Netatmo, потому что бывший использованный способ будет отключен в октябре 2022 года. Более старые версии адаптера могут больше не работать.", 188 | "pt": "Por causa das mudanças de API necessárias e também por causa da disponibilidade de mais e mais dispositivos Netatmo decidimos mudar a estrutura do objeto. Começando com a versão 2.0 os IDs exclusivos são usados em vez dos nomes dos dispositivos. Por causa disso, a estrutura do objeto mudará completamente. Por favor, exclua os objetos atuais idealmente antes da atualização. Além disso, a autenticação com Netatmo foi alterada porque a antiga maneira usada será desativada em outubro de 2022. Versões mais antigas do adaptador podem não funcionar mais.", 189 | "nl": "Omdat API veranderingen nodig had en ook vanwege de vermogen van meer en meer Netatmo apparaten die we besloten hebben om de objectstructuur te veranderen. Begin met versie 2.0 de unieke ID's worden gebruikt in plaats van de apparaatnamen. Hierdoor zal de objectstructuur volledig veranderen. Verwijder de huidige objecten ideaal voor de upgrade. De authenticatie met Netatmo is veranderd omdat de vroegere manier gehandicapt zal worden in oktober 2022. Oudere versies van de adapter werken misschien niet meer.", 190 | "fr": "En raison des changements d'API nécessaires et aussi en raison de la disponibilité de plus en plus de Netatmo appareils nous avons décidé de changer la structure de l'objet. À partir de la version 2.0, les identifiants uniques sont utilisés au lieu des noms de périphériques. En raison de cela la structure de l'objet va changer complètement. Veuillez supprimer les objets courants idéalement avant la mise à niveau. En outre, l'authentification avec Netatmo a été changée parce que l'ancienne méthode utilisée sera désactivée en octobre 2022. Les versions plus anciennes de l'adaptateur pourraient ne plus fonctionner.", 191 | "it": "A causa dei necessari cambiamenti API e anche a causa della disponibilità di sempre più dispositivi Netatmo abbiamo deciso di cambiare la struttura dell'oggetto. A partire dalla versione 2.0 gli ID unici sono utilizzati invece dei nomi dei dispositivi. A causa di questo la struttura dell'oggetto cambierà completamente. Si prega di eliminare gli oggetti attuali idealmente prima dell'aggiornamento. Inoltre, l'autenticazione con Netatmo è stata modificata perché il primo modo usato sarà disabilitato nell'ottobre 2022. Le versioni più vecchie dell'adattatore potrebbero non funzionare più.", 192 | "es": "Debido a los cambios necesarios de API y también debido a la disponibilidad de más y más dispositivos Netatmo decidimos cambiar la estructura del objeto. A partir de la versión 2.0 se utilizan los IDs únicos en lugar de los nombres de los dispositivos. Debido a esto la estructura del objeto cambiará completamente. Por favor, eliminar los objetos actuales idealmente antes de la actualización. Además, la autenticación con Netatmo se cambió porque la antigua forma utilizada será deshabilitada en octubre de 2022. Las versiones más antiguas del adaptador podrían no funcionar más.", 193 | "pl": "Z powodu potrzebnych zmian API, a także z powodu dostępności częściej i innych urządzeń Netatmo zdecydowały się zmienić strukturę obiektu. Począwszy od wersji 2.0 używa się unikalnych identyfikatorów zamiast nazw urządzenia. Z tego powodu struktura obiektu zmienia się całkowicie. Okazuje się, że aktualne obiekty są idealne przed ulepszaniem. Zmieniono również autentyczność z Netatmo, ponieważ w październiku 2022 r. zostaną zdegradowane. Starsze wersje adaptera nie mogą już pracować.", 194 | "zh-cn": "由于需要的知识产权变化,而且由于我们决定改变目标结构的更多和更净的装置。 从第2.0版本开始,独有的开发者不用装置名称。 由于这个目标结构将完全改变。 请在升级之前删除目前的目标。 此外,由于原用方式将于2022年10月残疾,净树的核证有所改变。 适应者的旧版本可能不再工作。.", 195 | "uk": "Через необхідні зміни в API, а також через доступність все більшої кількості пристроїв Netatmo ми вирішили змінити структуру об’єкта. Починаючи з версії 2.0 замість імен пристроїв використовуються унікальні ідентифікатори. Через це повністю зміниться структура об'єкта. В ідеалі видаліть поточні об’єкти перед оновленням. Крім того, змінено автентифікацію за допомогою Netatmo, оскільки попередній спосіб буде вимкнено в жовтні 2022 року. Старіші версії адаптера можуть більше не працювати." 196 | }, 197 | "level": "warn", 198 | "buttons": [ 199 | "agree", 200 | "cancel" 201 | ] 202 | }, 203 | { 204 | "condition": { 205 | "operand": "and", 206 | "rules": [ 207 | "oldVersion<3.0.0", 208 | "newVersion>=3.0.0" 209 | ] 210 | }, 211 | "title": { 212 | "en": "Realtime events now require an iot License", 213 | "de": "Realtime-Ereignisse benötigen jetzt eine iot-Lizenz", 214 | "ru": "Мероприятия в режиме реального времени теперь требуют лицензии iot", 215 | "pt": "Eventos em tempo real agora exigem uma licença iot", 216 | "nl": "Realtime evenementen hebben nu een iot License nodig", 217 | "fr": "Les événements en temps réel nécessitent maintenant une licence iot", 218 | "it": "Gli eventi in tempo reale richiedono ora una licenza iot", 219 | "es": "Eventos en tiempo real ahora requieren una licencia iot", 220 | "pl": "Wydarzenia rzeczywiste wymagają teraz iot License", 221 | "uk": "В режимі реального часу зажадає iot ліцензія", 222 | "zh-cn": "实际事件现在需要一分钟的Lcense" 223 | }, 224 | "text": { 225 | "en": "To receive realtime events from Netatmo for your Welcome/Presence, Doorbell or CO2/Smoke Sensors you need a Pro Cloud Account with an Assistent- or Remote-License and an installed iot instance connected to this account. The iot instance needs to have v1.14.0 or higher.", 226 | "de": "Um Echtzeit-Events von Netatmo für die Welcome/Presence, Doorbell oder CO2/Smoke Sensoren zu erhalten, wird ein Pro Cloud-Konto mit einer Assistenten- oder Remote-License und eine installierte Iot-Instanz benötigt, die mit diesem Konto verbunden ist. Die iot Instanz muss v1.14.0 oder höher haben.", 227 | "ru": "Чтобы получать события в режиме реального времени от Netatmo для вашего Welcome/Presence, Doorbell или CO2/Smoke Sensors, вам нужен Pro Cloud Account с помощью Assistent- или Remote-License и установленной iot-пример, подключенный к этой учетной записи. Иот пример должен иметь v1.14.0 или выше.", 228 | "pt": "Para receber eventos em tempo real da Netatmo para seus sensores de boas-vindas/presença, campainha ou CO2/Smoke, você precisa de uma conta Pro Cloud com um Assistent- ou Remote-License e uma instância de iot instalada conectada a esta conta. A instância iot precisa ter v1.14.0 ou superior.", 229 | "nl": "Om echte gebeurtenissen van Netatmo te ontvangen voor uw welkom/Presence, Deurbell of CO2/Smoke Sensors je hebt een Pro Cloud Account nodig met een assistent of Remote-Lic geïnstalleerd met deze rekening. Het iot instance moet v1.14.0 of hoger.", 230 | "fr": "Pour recevoir des événements en temps réel de Netatmo pour votre Welcome/Presence, Doorbell ou CO2/Smoke Sensors, vous avez besoin d'un Compte Pro Cloud avec un Helpent- ou Remote-License et une instance iot installée connectée à ce compte. L'instance iot doit avoir v1.14.0 ou supérieur.", 231 | "it": "Per ricevere eventi in tempo reale da Netatmo per il tuo Welcome/Presence, Doorbell o CO2/Smoke Sensors è necessario un account Pro Cloud con un Assistent- o Remote-License e un'istanza di iot installata collegata a questo account. L'istanza iot deve avere v1.14.0 o superiore.", 232 | "es": "Para recibir eventos en tiempo real de Netatmo para su Sensores de Bienvenida/Presencia, Doorbell o CO2/Smoke necesita una cuenta Pro Cloud con un Assistent- or Remote-License y una instancia iot instalada conectada a esta cuenta. La instancia iot debe tener v1.14.0 o superior.", 233 | "pl": "Aby otrzymać rzeczywiste wydarzenia z Netatmo dla twoich Welcome/Presence, Doorbell lub CO2/Smoke Sensors potrzebują księgozbiorów Pro Cloud Account z Assistent- lub Remote-License oraz zainstalowanym iotem. Na przykład iot musi mieć v1.14.0 lub więcej.", 234 | "uk": "Щоб отримувати події в режимі реального часу від Netatmo для вашого Welcome/Presence, Doorbell або CO2/Smoke Sensors, вам потрібно обліковий запис Pro Cloud з Assistent- або Remote-License і встановленим екземпляром iot, підключеним до цього облікового запису. Іо екземпляр повинен мати v1.14.0 або вище.", 235 | "zh-cn": "你们需要一个辅助性或遥感账户和一个与该账户有关的安装的iot例子,以便从网上摩托运到实时活动。 举例需要有1 1104.0或更高。." 236 | }, 237 | "level": "warn", 238 | "buttons": [ 239 | "agree", 240 | "cancel" 241 | ], 242 | "link": "https://github.com/PArns/ioBroker.netatmo#readme", 243 | "linkText": { 244 | "en": "More details", 245 | "de": "Mehr Details", 246 | "ru": "Подробнее", 247 | "pt": "Mais detalhes", 248 | "nl": "Meer details", 249 | "fr": "Plus de détails", 250 | "it": "Maggiori dettagli", 251 | "es": "Más detalles", 252 | "pl": "Szczegóły", 253 | "uk": "Детальніше", 254 | "zh-cn": "详情" 255 | } 256 | } 257 | ], 258 | "licenseInformation": { 259 | "type": "free", 260 | "license": "MIT" 261 | } 262 | }, 263 | "protectedNative": [ 264 | "secret", 265 | "id", 266 | "username", 267 | "password" 268 | ], 269 | "native": { 270 | "netatmoWeather": true, 271 | "netatmoWelcome": false, 272 | "netatmoCoach": false, 273 | "netatmoSmokedetector": false, 274 | "netatmoCOSensor": false, 275 | "netatmoDoorBell": false, 276 | "netatmoBubendorff": false, 277 | "check_interval": 5, 278 | "cleanup_interval": 60, 279 | "event_time": 12, 280 | "unknown_person_time": 24, 281 | "location_elevation": 0, 282 | "id": "", 283 | "secret": "", 284 | "iotInstance": "" 285 | }, 286 | "objects": [] 287 | } 288 | -------------------------------------------------------------------------------- /admin/words.js: -------------------------------------------------------------------------------- 1 | /*global systemDictionary:true */ 2 | /* 3 | +===================== DO NOT MODIFY ======================+ 4 | | This file was generated by translate-adapter, please use | 5 | | `translate-adapter adminLanguages2words` to update it. | 6 | +===================== DO NOT MODIFY ======================+ 7 | */ 8 | 'use strict'; 9 | 10 | systemDictionary = { 11 | "Authentication information": { "en": "Please press the above button to authenticate with your Netatmo Account to allow to access the data. You need to authenticate again whenever you add more device-types to your account, else you might miss data!", "de": "Mit dem obigen Button erfolgt eine Authentifizierung mit dem Netatmo-Konto, um Zugriff auf die Daten zu ermöglichen. Dies muss wiederholt werden, wenn weitere Gerätetypen zum Netatmo-Konto hinzugefügt werden, sonst könnten Daten fehlen!", "ru": "Пожалуйста, нажмите кнопку выше, чтобы подтвердить подлинность вашей учетной записи Netatmo, чтобы позволить получить доступ к данным. Вам нужно снова аутентифицировать, когда вы добавляете больше типов устройств на свой счет, еще вы можете пропустить данные!", "pt": "Por favor, pressione o botão acima para autenticar com sua Conta Netatmo para permitir acessar os dados. Você precisa autenticar novamente sempre que adicionar mais tipos de dispositivo à sua conta, então você pode perder dados!", "nl": "Druk op de bovenste knop om te authenticeren met je Netatmo Account om toegang te krijgen tot de gegevens. Je moet opnieuw authenticeren wanneer je meer apparaat-types op je rekening voegt, anders mis je data!", "fr": "Veuillez appuyer sur le bouton ci-dessus pour vous authentifier avec votre compte Netatmo afin d'accéder aux données. Vous avez besoin d'authentifier à nouveau chaque fois que vous ajoutez d'autres types de périphériques à votre compte, sinon vous pourriez manquer des données!", "it": "Si prega di premere il pulsante sopra per autenticare con il proprio account Netatmo per consentire di accedere ai dati. È necessario autenticare di nuovo ogni volta che si aggiungono più tipi di dispositivo al tuo account, altrimenti si potrebbero perdere i dati!", "es": "Por favor, presione el botón anterior para autenticar con su cuenta Netatmo para permitir el acceso a los datos. Necesitas autenticar de nuevo cada vez que agregas más tipos de dispositivo a tu cuenta, ¡de lo contrario podrías perder datos!", "pl": "Proszę nad przyciskiem, aby uwierzyć w twoją Netatmo Account, aby umożliwić dostęp do danych. Znów potrzebujesz uwierzytelniania, kiedy dodasz więcej typów urządzenie do twojego konta, inni moglibyś znieść dane!", "uk": "Будь ласка, натисніть кнопку вище, щоб автентифікуватися за допомогою свого облікового запису Netatmo, щоб отримати доступ до даних. Щоразу, коли ви додаєте нові типи пристроїв до свого облікового запису, вам потрібно знову проходити автентифікацію, інакше ви можете втратити дані!", "zh-cn": "请将上述但有礼拜通知你的内联网mo帐户,以便获取数据。 你们如果你在你的账户中增加更多的装置型,你就可能错失数据!"}, 12 | "CO": { "en": "CO sensor", "de": "CO-Sensor", "ru": "Датчик угарного газа", "pt": "sensor de CO", "nl": "CO-sensor", "fr": "Capteur CO", "it": "Sensore di CO", "es": "sensor de monóxido de carbono", "pl": "Czujnik CO", "uk": "датчик CO", "zh-cn": "一氧化碳传感器"}, 13 | "CheckIntervall": { "en": "Data update interval", "de": "Datenaktualisierungsintervall", "ru": "Интервал обновления данных", "pt": "Intervalo de atualização de dados", "nl": "Interval voor gegevensupdate", "fr": "Intervalle de mise à jour des données", "it": "Intervallo di aggiornamento dei dati", "es": "Intervalo de actualización de datos", "pl": "Interwał aktualizacji danych", "uk": "Інтервал оновлення даних", "zh-cn": "数据更新间隔"}, 14 | "CleanupIntervall": { "en": "Data cleanup interval", "de": "Datenbereinigungsintervall", "ru": "Интервал очистки данных", "pt": "Intervalo de limpeza de dados", "nl": "Interval voor het opschonen van gegevens", "fr": "Intervalle de nettoyage des données", "it": "Intervallo di pulizia dei dati", "es": "Intervalo de limpieza de datos", "pl": "Interwał czyszczenia danych", "uk": "Інтервал очищення даних", "zh-cn": "数据清理间隔"}, 15 | "ClientID": { "en": "Individual Client ID", "de": "Individuelle Client-ID", "ru": "Индивидуальный идентификатор клиента", "pt": "ID de cliente individual", "nl": "Individuele klant-ID", "fr": "ID client individuel", "it": "ID cliente individuale", "es": "ID de cliente individual", "pl": "Indywidualny identyfikator klienta", "uk": "Індивідуальний ідентифікатор клієнта", "zh-cn": "个人客户 ID"}, 16 | "ClientSecret": { "en": "Individual Client secret", "de": "Individuelles Client-secret", "ru": "Индивидуальный секрет клиента", "pt": "Segredo do cliente individual", "nl": "Individueel klantgeheim", "fr": "Secret client individuel", "it": "Segreto del singolo cliente", "es": "Secreto de cliente individual", "pl": "Indywidualny sekret Klienta", "uk": "Індивідуальний секрет клієнта", "zh-cn": "个人客户秘密"}, 17 | "E-Mail": { "en": "E-Mail", "de": "Email", "ru": "Эл. почта", "pt": "O email", "nl": "E-mail", "fr": "E-mail", "it": "E-mail", "es": "Email", "pl": "E-mail", "uk": "Електронна пошта", "zh-cn": "电子邮件"}, 18 | "Elevation": { "en": "Elevation", "de": "Elevation", "ru": "Высота", "pt": "Elevação", "nl": "Verhoging", "fr": "Élévation", "it": "Elevazione", "es": "Elevación", "pl": "Podniesienie", "uk": "Висота", "zh-cn": "海拔"}, 19 | "Health Coach": { "en": "Healthy Home Coach", "de": "Smarter Luftqualitätssensor", "ru": "Тренер по здоровью", "pt": "Coach de Saúde", "nl": "Gezondheidscoach", "fr": "Coach Santé", "it": "Allenatore della salute", "es": "entrenador de salud", "pl": "Trener zdrowia", "uk": "Тренер здоров'я", "zh-cn": "健康教练"}, 20 | "Login with Netatmo": { "en": "Login with Netatmo", "de": "Login mit Netatmo", "ru": "Войти с Netatmo", "pt": "Login com Netatmo", "nl": "Login met Netatmo", "fr": "Connexion avec Netatmo", "it": "Accedi con Netatmo", "es": "Iniciar sesión con Netatmo", "pl": "Login", "uk": "Увійдіть за допомогою Netatmo", "zh-cn": "D. 净摩的物质"}, 21 | "Netatmo App": { "en": "From your Netatmo App", "de": "Über Ihre Netatmo-App", "ru": "Из вашего приложения Netatmo", "pt": "Do seu aplicativo Netatmo", "nl": "Vanuit uw Netatmo-app", "fr": "Depuis votre appli Netatmo", "it": "Dalla tua App Netatmo", "es": "Desde tu App Netatmo", "pl": "Z Twojej aplikacji Netatmo", "uk": "З вашого додатку Netatmo", "zh-cn": "从您的 Netatmo 应用程序"}, 22 | "Password": { "en": "Password", "de": "Passwort", "ru": "Пароль", "pt": "Senha", "nl": "Wachtwoord", "fr": "Mot de passe", "it": "Parola d'ordine", "es": "Clave", "pl": "Hasło", "uk": "Пароль", "zh-cn": "密码"}, 23 | "RemoveEvents": { "en": "Remove events older than", "de": "Ereignisse entfernen, die älter sind als", "ru": "Удалить события старше", "pt": "Remover eventos anteriores a", "nl": "Gebeurtenissen ouder dan . verwijderen", "fr": "Supprimer les événements antérieurs à", "it": "Rimuovi gli eventi più vecchi di", "es": "Eliminar eventos anteriores a", "pl": "Usuń wydarzenia starsze niż", "uk": "Видалити події, старші ніж", "zh-cn": "删除早于的事件"}, 24 | "RemoveUnknownPerson": { "en": "Remove unknown persons last seen older than", "de": "Unbekannte Personen entfernen, die zuletzt gesehen wurden, älter als", "ru": "Удалить неизвестных лиц, которых последний раз видели старше", "pt": "Remover pessoas desconhecidas vistas pela última vez com mais de", "nl": "Verwijder onbekende personen die het laatst gezien ouder zijn dan", "fr": "Supprimer les personnes inconnues vues pour la dernière fois plus de", "it": "Rimuovi le persone sconosciute viste l'ultima volta più vecchie di", "es": "Eliminar personas desconocidas vistas por última vez mayores de", "pl": "Usuń nieznane osoby, które ostatnio widziano starsze niż", "uk": "Видаліть невідомих людей, яких востаннє бачили старше ніж", "zh-cn": "删除最后一次见到的不知名人士"}, 25 | "Smokedetector": { "en": "smoke detector", "de": "Rauchmelder", "ru": "детектор дыма", "pt": "detector de fumaça", "nl": "rookdetector", "fr": "détecteur de fumée", "it": "rilevatore di fumo", "es": "detector de humo", "pl": "Czujnik dymu", "uk": "Детектор диму", "zh-cn": "烟雾探测器"}, 26 | "Weather station": { "en": "Weather station", "de": "Wetterstation", "ru": "Метеостанция", "pt": "Estação meteorológica", "nl": "Weerstation", "fr": "Station météo", "it": "Stazione metereologica", "es": "Estación meteorológica", "pl": "Stacja pogodowa", "uk": "Метеостанція", "zh-cn": "气象站"}, 27 | "Welcome indoor cam": { "en": "Welcome indoor/outdoor camera", "de": "Welcome Innen-/Aussenkamera", "ru": "Welcome внутренняя камера", "pt": "Welcome câmera interna/exterior", "nl": "Welcome binnencamera", "fr": "Caméra intérieure/extérieure Welcome", "it": "Welcome telecamera per interni/esterna", "es": "Welcome Cámara interior/exterior", "pl": "Welcome kamera wewnętrzna", "uk": "Ласкаво просимо внутрішню/зовнішню камеру", "zh-cn": "世界自然协会"}, 28 | "_additionalSettingsHeader": { "en": "Additional Settings", "de": "Zusätzliche Einstellungen", "ru": "Дополнительные настройки", "pt": "Configurações adicionais", "nl": "Aanvullende instellingen", "fr": "Paramètres additionnels", "it": "Altre impostazioni", "es": "Ajustes adicionales", "pl": "Dodatkowe ustawienia", "uk": "Додаткові налаштування", "zh-cn": "其他设置"}, 29 | "_realtimeEventHeader": { "en": "Realtime Events", "de": "Echtzeit-Ereignisse", "ru": "События в реальном времени", "pt": "Eventos em tempo real", "nl": "Realtime evenementen", "fr": "Événements en temps réel", "it": "Eventi in tempo reale", "es": "Eventos en tiempo real", "pl": "Zdarzenia w czasie rzeczywistym", "uk": "Події в реальному часі", "zh-cn": "实时事件"}, 30 | "_realtimeEventInfo": { "en": "In order to receive realtime events from Welcome & Presence or to get realtime smoke alerts, doorbell or the CO sensor, you need to select an iot Adapter instance that has an active Assistant or Remote License to pass through the event data!", "de": "Um Echtzeit-Ereignisse von Welcome & Presence oder Echtzeit-Rauchalarme, Türklingeln oder den CO-Sensor zu erhalten, muss eine iot Adapter-Instanz ausgewählt werden, die über eine aktive Assistenten- oder Remote-Lizenz verfügt, um die Ereignisdaten weiterzuleiten!", "ru": "Чтобы получать события в реальном времени от приложения «Добро пожаловать и присутствие» или получать оповещения о дыме, дверном звонке или датчике угарного газа в реальном времени, вам необходимо выбрать экземпляр IoT-адаптера с активным помощником или удаленной лицензией для передачи данных о событиях!", "pt": "Para receber eventos em tempo real de boas-vindas e presença ou para obter alertas de fumaça, campainha ou sensor de CO em tempo real, você precisa selecionar uma instância do adaptador iot que tenha um assistente ativo ou licença remota para passar os dados do evento!", "nl": "Om realtime gebeurtenissen van Welcome & Presence te ontvangen of om realtime rookmeldingen, deurbel of de CO-sensor te ontvangen, moet u een iot Adapter-instantie selecteren die een actieve assistent of licentie op afstand heeft om de gebeurtenisgegevens door te geven!", "fr": "Afin de recevoir des événements en temps réel de Welcome & Presence ou d'obtenir des alertes de fumée en temps réel, une sonnette ou le capteur de CO, vous devez sélectionner une instance d'adaptateur iot disposant d'un assistant actif ou d'une licence à distance pour transmettre les données d'événement !", "it": "Per ricevere eventi in tempo reale da Welcome & Presence o per ricevere avvisi di fumo in tempo reale, campanello o sensore di CO, è necessario selezionare un'istanza dell'adattatore iot con un assistente attivo o una licenza remota per passare attraverso i dati dell'evento!", "es": "Para recibir eventos en tiempo real de Bienvenida y presencia o recibir alertas de humo, timbre o sensor de CO en tiempo real, debe seleccionar una instancia de adaptador iot que tenga un asistente activo o una licencia remota para pasar los datos del evento.", "pl": "Aby otrzymywać zdarzenia w czasie rzeczywistym z Welcome & Presence lub otrzymywać powiadomienia o dymie, dzwonek do drzwi lub czujnik CO w czasie rzeczywistym, musisz wybrać instancję adaptera iot, która ma aktywnego asystenta lub licencję zdalną, aby przekazywać dane zdarzeń!", "uk": "Щоб отримувати події в режимі реального часу від Welcome & Presence або отримувати сповіщення про дим у реальному часі, дверний дзвінок або датчик CO, вам потрібно вибрати екземпляр iot Adapter, який має активний помічник або віддалену ліцензію для передачі даних про події!", "zh-cn": "为了从 Welcome & Presence 接收实时事件或获得实时烟雾警报、门铃或 CO 传感器,您需要选择一个具有活动助手或远程许可证的物联网适配器实例来传递事件数据!"}, 31 | "auth_info_individual_credentials": { "en": "After entering or changing an individual client-ID please repeat the Authentication process using the above button.", "de": "Nachdem Sie eine individuellen Client-ID eingegeben oder geändert haben, wiederholen Sie bitte den Authentifizierungsprozess mit der obigen Schaltfläche.", "ru": "После ввода или изменения выделенного идентификатора клиента повторите процесс аутентификации, используя кнопку выше.", "pt": "Depois de inserir ou alterar um ID de cliente dedicado, repita o processo de autenticação usando o botão acima.", "nl": "Na het invoeren of wijzigen van een toegewezen client-ID, herhaalt u het authenticatieproces met behulp van de bovenstaande knop.", "fr": "Après avoir saisi ou modifié un ID client dédié, veuillez répéter le processus d'authentification à l'aide du bouton ci-dessus.", "it": "Dopo aver inserito o modificato un ID cliente dedicato, ripetere il processo di autenticazione utilizzando il pulsante sopra.", "es": "Después de ingresar o cambiar una identificación de cliente dedicada, repita el proceso de autenticación usando el botón anterior.", "pl": "Po wprowadzeniu lub zmianie dedykowanego identyfikatora klienta należy powtórzyć proces Uwierzytelniania za pomocą powyższego przycisku.", "uk": "Після введення або зміни індивідуального ідентифікатора клієнта повторіть процес автентифікації за допомогою кнопки вище.", "zh-cn": "输入或更改专用客户端 ID 后,请使用上述按钮重复验证过程。"}, 32 | "clean minutes": { "en": "(minutes)", "de": "(Minuten)", "ru": "(минут)", "pt": "(minutos)", "nl": "(minuten)", "fr": "(minutes)", "it": "(minuti)", "es": "(minutos)", "pl": "(minuty)", "uk": "(хвилин)", "zh-cn": "(分钟)"}, 33 | "hours": { "en": "(hours)", "de": "(Stunden)", "ru": "(часы)", "pt": "(horas)", "nl": "(uur)", "fr": "(les heures)", "it": "(ore)", "es": "(horas)", "pl": "(godziny)", "uk": "(годин)", "zh-cn": "(小时)"}, 34 | "iotInstanceLabel": { "en": "iot Instance for Realtime Events", "de": "iot-Instanz für Echtzeitereignisse", "ru": "Инстанс iot для событий в реальном времени", "pt": "instância iot para eventos em tempo real", "nl": "iot-instantie voor realtime gebeurtenissen", "fr": "Instance iot pour les événements en temps réel", "it": "Istanza iot per eventi in tempo reale", "es": "Instancia iot para eventos en tiempo real", "pl": "Instancja iot dla zdarzeń w czasie rzeczywistym", "uk": "Екземпляр iot для подій у реальному часі", "zh-cn": "实时事件的物联网实例"}, 35 | "live_stream1": { "en": "In order to increase update interval or receive livestreams from Welcome & Presence or to get realtime smoke alerts, doorbell or the CO sensor, you've to request your own API key from Netatmo!", "de": "Um das Aktualisierungsintervall zu erhöhen oder Livestreams von Welcome & Anwesenheit oder um Echtzeit-Rauchalarme, Türklingel oder den CO-Sensor zu erhalten, müssen Sie Ihren eigenen API-Schlüssel von Netatmo anfordern!", "ru": "Чтобы увеличить интервал обновления или получать прямые трансляции от Welcome & Присутствие или для получения оповещений о дыме в реальном времени, дверном звонке или датчике CO, вы должны запросить свой собственный ключ API от Netatmo!", "pt": "Para aumentar o intervalo de atualização ou receber transmissões ao vivo de Welcome & Presença ou para obter alertas de fumaça em tempo real, campainha ou sensor de CO, você deve solicitar sua própria chave de API da Netatmo!", "nl": "Om het update-interval te verlengen of livestreams te ontvangen van Welcome & Aanwezigheid of om realtime rookwaarschuwingen, deurbel of de CO-sensor te krijgen, moet u uw eigen API-sleutel bij Netatmo aanvragen!", "fr": "Afin d'augmenter l'intervalle de mise à jour ou de recevoir des flux en direct de Welcome & Présence ou pour recevoir les alertes fumées en temps réel, la sonnette ou le capteur de CO, il faut demander sa propre clé API à Netatmo !", "it": "Per aumentare l'intervallo di aggiornamento o ricevere live streaming da Welcome & Presenza o per ricevere avvisi di fumo in tempo reale, campanello o sensore di CO, devi richiedere la tua chiave API a Netatmo!", "es": "Para aumentar el intervalo de actualización o recibir transmisiones en vivo de Welcome & Presencia o para recibir alertas de humo en tiempo real, timbre o el sensor de CO, ¡debe solicitar su propia clave API de Netatmo!", "pl": "Aby wydłużyć interwał aktualizacji lub odbierać transmisje na żywo z Welcome & Obecność lub aby otrzymywać powiadomienia o dymie w czasie rzeczywistym, dzwonek do drzwi lub czujnik CO, musisz poprosić Netatmo o własny klucz API!", "uk": "Щоб збільшити інтервал оновлення або отримувати прямі трансляції від Welcome & Presence або отримувати сповіщення про дим у реальному часі, дверний дзвінок чи датчик CO, ви повинні запросити власний ключ API від Netatmo!", "zh-cn": "为了增加更新间隔或接收来自 Welcome &存在或获取实时烟雾警报、门铃或 CO 传感器,您必须向 Netatmo 请求您自己的 API 密钥!"}, 36 | "live_stream2": { "en": "To do so, go to the following URL, login with your Netatmo account and fill out the requested form", "de": "Gehen Sie dazu auf die folgende URL, melden Sie sich mit Ihrem Netatmo-Konto an und füllen Sie das angeforderte Formular aus", "ru": "Для этого перейдите по следующему URL-адресу, войдите в свою учетную запись Netatmo и заполните требуемую форму.", "pt": "Para isso, acesse a seguinte URL, faça login com sua conta Netatmo e preencha o formulário solicitado", "nl": "Ga hiervoor naar de volgende URL, log in met uw Netatmo-account en vul het gevraagde formulier in", "fr": "Pour cela, rendez-vous sur l'URL suivante, connectez-vous avec votre compte Netatmo et remplissez le formulaire demandé", "it": "Per farlo, vai al seguente URL, accedi con il tuo account Netatmo e compila il modulo richiesto", "es": "Para ello, acceda a la siguiente URL, inicie sesión con su cuenta Netatmo y rellene el formulario solicitado", "pl": "Aby to zrobić, przejdź pod następujący adres URL, zaloguj się na swoje konto Netatmo i wypełnij żądany formularz", "uk": "Для цього перейдіть за наведеною нижче URL-адресою, увійдіть у свій обліковий запис Netatmo та заповніть необхідну форму", "zh-cn": "为此,请访问以下 URL,使用您的 Netatmo 帐户登录并填写请求的表格"}, 37 | "load minutes": { "en": "(minutes)", "de": "(Minuten)", "ru": "(минут)", "pt": "(minutos)", "nl": "(minuten)", "fr": "(minutes)", "it": "(minuti)", "es": "(minutos)", "pl": "(minuty)", "uk": "(хвилин)", "zh-cn": "(分钟)"}, 38 | "meters": { "en": "meters over nN", "de": "Meter über nN", "ru": "метров свыше нН", "pt": "metros sobre nN", "nl": "meter boven nN", "fr": "mètres sur nN", "it": "metri su nN", "es": "metros sobre nN", "pl": "metry ponad nN", "uk": "метрів", "zh-cn": "米超过 nN"}, 39 | "netatmoBubendorff": { "en": "iDiamant/Bubendorff", "de": "iDiamant/Bubendorff", "ru": "iDiamant/Бубендорф", "pt": "iDiamant/Bubendorff", "nl": "iDiamant/Bubendorff", "fr": "iDiamant/Bubendorff", "it": "iDiamant/Bubendorff", "es": "iDiamant/Bubendorff", "pl": "iDiamant/Bubendorff", "uk": "iDiamant/Bubendorff", "zh-cn": "iDiamant/布本多夫"}, 40 | }; -------------------------------------------------------------------------------- /lib/netatmoCoach.js: -------------------------------------------------------------------------------- 1 | module.exports = function (myapi, myadapter) { 2 | const api = myapi; 3 | const adapter = myadapter; 4 | 5 | let finalized = false; 6 | 7 | const DewPoint = require('dewpoint'); 8 | 9 | this.finalize = function () { 10 | finalized = true; 11 | }; 12 | 13 | this.requestUpdateCoachStation = function () { 14 | api.getCoachData({}, async (err, data) => { 15 | if (finalized) return; 16 | adapter.log.debug(`Get Coach data: ${JSON.stringify(data)}`); 17 | if (err === null) { 18 | if (data) { 19 | await adapter.extendOrSetObjectNotExistsAsync('AirQuality', { 20 | type: 'folder', 21 | common: { 22 | name: 'Air quality devices', 23 | }, 24 | native: { 25 | } 26 | }); 27 | if (Array.isArray(data)) { 28 | for (const aDevice of data) { 29 | await handleDevice(aDevice,'AirQuality'); 30 | } 31 | } else if (data) { 32 | await handleDevice(data, 'AirQuality'); 33 | } 34 | } 35 | } 36 | }); 37 | }; 38 | 39 | /* 40 | function formatDeviceName(aDeviceName) { 41 | if (!aDeviceName) { 42 | return 'Unnamed'; 43 | } 44 | 45 | return aDeviceName.replace(/ /g, '-').replace(/---/g, '-').replace(/--/g, '-').replace(adapter.FORBIDDEN_CHARS, '_').replace(/\s|\./g, '_'); 46 | } 47 | */ 48 | 49 | async function handleDevice(aDevice, aParent) { 50 | const deviceId = aDevice._id.replace(/:/g, '-'); // formatDeviceName(aDevice.station_name || aDevice.name); 51 | aParent = aParent ? `${aParent}.${deviceId}` : deviceId; 52 | await adapter.extendOrSetObjectNotExistsAsync(aParent, { 53 | type: 'device', 54 | common: { 55 | name: aDevice.station_name || aDevice.name || 'Unnamed', 56 | }, 57 | native: { 58 | id: aDevice._id, 59 | type: aDevice.type 60 | } 61 | }); 62 | await handleCoachModule(aDevice, aParent); 63 | } 64 | 65 | async function handleCoachModule(aModule, aParent) { 66 | for (const aDeviceType of aModule.data_type) { 67 | switch (aDeviceType) { 68 | case 'Temperature': 69 | await handleTemperature(aModule, aParent); 70 | break; 71 | case 'CO2': 72 | await handleCO2(aModule, aParent); 73 | break; 74 | case 'Humidity': 75 | await handleHumidity(aModule, aParent); 76 | break; 77 | case 'Noise': 78 | await handleNoise(aModule, aParent); 79 | break; 80 | case 'Pressure': 81 | await handlePressure(aModule, aParent); 82 | break; 83 | case 'health_idx': 84 | await handleHealthIdx(aModule, aParent); 85 | break; 86 | default: 87 | adapter.log.info(`UNKNOWN DEVICE TYPE: ${aDeviceType} ${JSON.stringify(aModule)}`); 88 | break; 89 | } 90 | } 91 | 92 | if (aModule.dashboard_data && aModule.dashboard_data.Temperature !== undefined && aModule.dashboard_data.Humidity !== undefined) { 93 | const dp = new DewPoint(myadapter.config.location_elevation); 94 | const point = dp.Calc(+aModule.dashboard_data.Temperature, +aModule.dashboard_data.Humidity); 95 | 96 | await adapter.extendOrSetObjectNotExistsAsync(`${aParent}.Temperature.DewPoint`, { 97 | type: 'state', 98 | common: { 99 | name: 'Dew point temperature', 100 | type: 'number', 101 | role: 'value.temperature.dewpoint', 102 | read: true, 103 | write: false, 104 | unit: '°C' 105 | } 106 | }); 107 | 108 | await adapter.extendOrSetObjectNotExistsAsync(`${aParent}.Humidity.AbsoluteHumidity`, { 109 | type: 'state', 110 | common: { 111 | name: 'Absolute humidity in gram per kilogram air', 112 | type: 'number', 113 | role: 'value.humidity', 114 | read: true, 115 | write: false, 116 | unit: 'g/kg' 117 | } 118 | }); 119 | 120 | await adapter.setStateAsync(`${aParent}.Temperature.DewPoint`, {val: parseFloat(point.dp.toFixed(1)), ack: true}); 121 | await adapter.setStateAsync(`${aParent}.Humidity.AbsoluteHumidity`, {val: parseFloat(point.x.toFixed(3)), ack: true}); 122 | } 123 | 124 | if (aModule._id) { 125 | await adapter.extendOrSetObjectNotExistsAsync(`${aParent}._id`, { 126 | type: 'state', 127 | common: { 128 | name: '_id', 129 | type: 'string', 130 | role: 'info.address', 131 | read: true, 132 | write: false 133 | } 134 | }); 135 | 136 | await adapter.setStateAsync(`${aParent}._id`, {val: aModule._id, ack: true}); 137 | } 138 | 139 | if (aModule.last_status_store) { 140 | const theDate = new Date(aModule.last_status_store * 1000); 141 | 142 | await adapter.extendOrSetObjectNotExistsAsync(`${aParent}.LastUpdate`, { 143 | type: 'state', 144 | common: { 145 | name: 'Last update', 146 | type: 'string', 147 | role: 'value.date', 148 | read: true, 149 | write: false 150 | } 151 | }); 152 | 153 | await adapter.setStateAsync(`${aParent}.LastUpdate`, {val: theDate.toString(), ack: true}); 154 | } 155 | 156 | if (aModule.place.city) { 157 | await handlePlace(aModule, aParent); 158 | } 159 | 160 | if (aModule.type) { 161 | await adapter.extendOrSetObjectNotExistsAsync(`${aParent}.type`, { 162 | type: 'state', 163 | common: { 164 | name: 'type', 165 | type: 'string', 166 | role: 'state', 167 | read: true, 168 | write: false 169 | } 170 | }); 171 | 172 | await adapter.setStateAsync(`${aParent}.type`, {val: aModule.type, ack: true}); 173 | } 174 | 175 | if (aModule.date_setup) { 176 | const theDate4 = new Date(aModule.date_setup * 1000); 177 | 178 | await adapter.extendOrSetObjectNotExistsAsync(`${aParent}.date_setup`, { 179 | type: 'state', 180 | common: { 181 | name: 'date setup', 182 | type: 'string', 183 | role: 'value.date', 184 | read: true, 185 | write: false 186 | } 187 | }); 188 | 189 | await adapter.setStateAsync(`${aParent}.date_setup`, {val: theDate4.toString(), ack: true}); 190 | } 191 | 192 | if (aModule.last_setup) { 193 | const theDate3 = new Date(aModule.last_setup * 1000); 194 | 195 | await adapter.extendOrSetObjectNotExistsAsync(`${aParent}.last_setup`, { 196 | type: 'state', 197 | common: { 198 | name: 'Last setup', 199 | type: 'string', 200 | role: 'value.date', 201 | read: true, 202 | write: false 203 | } 204 | }); 205 | 206 | await adapter.setStateAsync(`${aParent}.last_setup`, {val: theDate3.toString(), ack: true}); 207 | } 208 | 209 | if (aModule.firmware) { 210 | await adapter.extendOrSetObjectNotExistsAsync(`${aParent}.firmware`, { 211 | type: 'state', 212 | common: { 213 | name: 'firmware', 214 | type: 'number', 215 | role: 'state', 216 | read: true, 217 | write: false 218 | } 219 | }); 220 | 221 | await adapter.setStateAsync(`${aParent}.firmware`, {val: aModule.firmware, ack: true}); 222 | } 223 | 224 | if (aModule.last_upgrade) { 225 | const theDate2 = new Date(aModule.last_upgrade * 1000); 226 | 227 | await adapter.extendOrSetObjectNotExistsAsync(`${aParent}.Last_fw_Upgrade`, { 228 | type: 'state', 229 | common: { 230 | name: 'Last firmware upgrade', 231 | type: 'string', 232 | role: 'value.date', 233 | read: true, 234 | write: false 235 | } 236 | }); 237 | 238 | await adapter.setStateAsync(`${aParent}.Last_fw_Upgrade`, {val: theDate2.toString(), ack: true}); 239 | } 240 | 241 | if (aModule.wifi_status) { 242 | let wifi_status = 'good'; 243 | 244 | if (aModule.wifi_status > 85) { 245 | wifi_status = 'bad'; 246 | } 247 | else if (aModule.wifi_status > 70) { 248 | wifi_status = 'average'; 249 | } 250 | 251 | await adapter.extendOrSetObjectNotExistsAsync(`${aParent}.wifi_status`, { 252 | type: 'state', 253 | common: { 254 | name: 'WiFi status', 255 | type: 'string', 256 | states: {'good': 'good', 'average': 'average', 'bad': 'bad'}, 257 | role: 'state', 258 | read: true, 259 | write: false 260 | } 261 | }); 262 | 263 | await adapter.setStateAsync(`${aParent}.wifi_status`, {val: wifi_status, ack: true}); 264 | 265 | await adapter.extendOrSetObjectNotExistsAsync(`${aParent}.wifi_status_num`, { 266 | type: 'state', 267 | common: { 268 | name: 'WiFi status NUM', 269 | type: 'number', 270 | role: 'value', 271 | read: true, 272 | write: false 273 | } 274 | }); 275 | 276 | await adapter.setStateAsync(`${aParent}.wifi_status_num`, {val: aModule.wifi_status, ack: true}); 277 | } 278 | } 279 | 280 | async function handleTemperature(aModule, aParent) { 281 | aParent += '.Temperature'; 282 | if (!aModule.dashboard_data) 283 | return; 284 | 285 | if (aModule.dashboard_data.Temperature !== undefined) { 286 | await adapter.extendOrSetObjectNotExistsAsync(aParent, { 287 | type: 'channel', 288 | common: { 289 | name: 'Temperature', 290 | } 291 | }); 292 | 293 | await adapter.extendOrSetObjectNotExistsAsync(`${aParent}.Temperature`, { 294 | type: 'state', 295 | common: { 296 | name: 'Temperature', 297 | type: 'number', 298 | role: 'value.temperature', 299 | read: true, 300 | write: false, 301 | unit: '°C' 302 | } 303 | }); 304 | 305 | await adapter.setStateAsync(`${aParent}.Temperature`, {val: aModule.dashboard_data.Temperature, ack: true}); 306 | 307 | await adapter.extendOrSetObjectNotExistsAsync(`${aParent}.TemperatureAbsoluteMin`, { 308 | type: 'state', 309 | common: { 310 | name: 'Absolute temperature minimum', 311 | type: 'number', 312 | role: 'value.temperature.min', 313 | read: true, 314 | write: false, 315 | unit: '°C' 316 | } 317 | }); 318 | 319 | await adapter.extendOrSetObjectNotExistsAsync(`${aParent}.TemperatureAbsoluteMax`, { 320 | type: 'state', 321 | common: { 322 | name: 'Absolute temperature maximum', 323 | type: 'number', 324 | role: 'value.temperature.max', 325 | read: true, 326 | write: false, 327 | unit: '°C' 328 | } 329 | }); 330 | 331 | await adapter.extendOrSetObjectNotExistsAsync(`${aParent}.TemperatureAbsoluteMinDate`, { 332 | type: 'state', 333 | common: { 334 | name: 'Absolute temperature maximum date', 335 | type: 'string', 336 | role: 'value.date', 337 | read: true, 338 | write: false 339 | } 340 | }); 341 | 342 | await adapter.extendOrSetObjectNotExistsAsync(`${aParent}.TemperatureAbsoluteMaxDate`, { 343 | type: 'state', 344 | common: { 345 | name: 'Absolute temperature maximum date', 346 | type: 'string', 347 | role: 'value.date', 348 | read: true, 349 | write: false 350 | } 351 | }); 352 | 353 | adapter.getState(`${aParent}.TemperatureAbsoluteMin`, async (err, state) => { 354 | if (!state || state.val > aModule.dashboard_data.Temperature) { 355 | await adapter.setStateAsync(`${aParent}.TemperatureAbsoluteMin`, { 356 | val: aModule.dashboard_data.Temperature, 357 | ack: true 358 | }); 359 | await adapter.setStateAsync(`${aParent}.TemperatureAbsoluteMinDate`, { 360 | val: new Date().toString(), 361 | ack: true 362 | }); 363 | } 364 | }); 365 | 366 | adapter.getState(`${aParent}.TemperatureAbsoluteMax`, async (err, state) => { 367 | if (!state || state.val < aModule.dashboard_data.Temperature) { 368 | await adapter.setStateAsync(`${aParent}.TemperatureAbsoluteMax`, { 369 | val: aModule.dashboard_data.Temperature, 370 | ack: true 371 | }); 372 | await adapter.setStateAsync(`${aParent}.TemperatureAbsoluteMaxDate`, { 373 | val: new Date().toString(), 374 | ack: true 375 | }); 376 | } 377 | }); 378 | } 379 | 380 | if (aModule.dashboard_data.min_temp !== undefined) { 381 | await adapter.extendOrSetObjectNotExistsAsync(`${aParent}.TemperatureMin`, { 382 | type: 'state', 383 | common: { 384 | name: 'Temperature minimum', 385 | type: 'number', 386 | role: 'value.temperature.min', 387 | read: true, 388 | write: false, 389 | unit: '°C' 390 | } 391 | }); 392 | 393 | await adapter.setStateAsync(`${aParent}.TemperatureMin`, {val: aModule.dashboard_data.min_temp, ack: true}); 394 | } 395 | 396 | if (aModule.dashboard_data.date_min_temp !== undefined) { 397 | await adapter.extendOrSetObjectNotExistsAsync(`${aParent}.TemperatureMinDate`, { 398 | type: 'state', 399 | common: { 400 | name: 'Temperature minimum date', 401 | type: 'string', 402 | role: 'value.date', 403 | read: true, 404 | write: false 405 | } 406 | }); 407 | 408 | await adapter.setStateAsync(`${aParent}.TemperatureMinDate`, { 409 | val: (new Date(aModule.dashboard_data.date_min_temp * 1000)).toString(), 410 | ack: true 411 | }); 412 | } 413 | 414 | if (aModule.dashboard_data.max_temp !== undefined) { 415 | await adapter.extendOrSetObjectNotExistsAsync(`${aParent}.TemperatureMax`, { 416 | type: 'state', 417 | common: { 418 | name: 'Temperature maximum', 419 | type: 'number', 420 | role: 'value.temperature.max', 421 | read: true, 422 | write: false, 423 | unit: '°C' 424 | } 425 | }); 426 | 427 | await adapter.setStateAsync(`${aParent}.TemperatureMax`, {val: aModule.dashboard_data.max_temp, ack: true}); 428 | } 429 | 430 | if (aModule.dashboard_data.date_max_temp !== undefined) { 431 | await adapter.extendOrSetObjectNotExistsAsync(`${aParent}.TemperatureMaxDate`, { 432 | type: 'state', 433 | common: { 434 | name: 'Temperature maximum date', 435 | type: 'string', 436 | role: 'value.date', 437 | read: true, 438 | write: false 439 | } 440 | }); 441 | 442 | await adapter.setStateAsync(`${aParent}.TemperatureMaxDate`, { 443 | val: (new Date(aModule.dashboard_data.date_max_temp * 1000)).toString(), 444 | ack: true 445 | }); 446 | } 447 | 448 | if (aModule.dashboard_data.temp_trend !== undefined) { 449 | await adapter.extendOrSetObjectNotExistsAsync(`${aParent}.TemperatureTrend`, { 450 | type: 'state', 451 | common: { 452 | name: 'Temperature trend', 453 | type: 'string', 454 | role: 'value.direction', 455 | read: true, 456 | write: false 457 | } 458 | }); 459 | 460 | await adapter.setStateAsync(`${aParent}.TemperatureTrend`, {val: aModule.dashboard_data.temp_trend, ack: true}); 461 | } 462 | } 463 | 464 | async function handleCO2(aModule, aParent) { 465 | aParent += '.CO2'; 466 | 467 | if (!aModule.dashboard_data) { 468 | return; 469 | } 470 | 471 | if (aModule.dashboard_data.CO2 !== undefined) { 472 | await adapter.extendOrSetObjectNotExistsAsync(aParent, { 473 | type: 'channel', 474 | common: { 475 | name: 'CO2', 476 | } 477 | }); 478 | 479 | await adapter.extendOrSetObjectNotExistsAsync(`${aParent}.CO2`, { 480 | type: 'state', 481 | common: { 482 | name: 'CO2', 483 | type: 'number', 484 | role: 'value', 485 | read: true, 486 | write: false, 487 | unit: 'ppm' 488 | } 489 | }); 490 | 491 | await adapter.setStateAsync(`${aParent}.CO2`, {val: aModule.dashboard_data.CO2, ack: true}); 492 | } 493 | 494 | if (aModule.co2_calibrating !== undefined) { 495 | await adapter.extendOrSetObjectNotExistsAsync(`${aParent}.Calibrating`, { 496 | type: 'state', 497 | common: { 498 | name: 'Calibrating', 499 | type: 'boolean', 500 | role: 'indicator', 501 | read: true, 502 | write: false 503 | } 504 | }); 505 | 506 | await adapter.setStateAsync(`${aParent}.Calibrating`, {val: aModule.co2_calibrating, ack: true}); 507 | } 508 | } 509 | 510 | async function handleHumidity(aModule, aParent) { 511 | aParent += '.Humidity'; 512 | 513 | if (!aModule.dashboard_data) { 514 | return; 515 | } 516 | 517 | if (aModule.dashboard_data.Humidity !== undefined) { 518 | await adapter.extendOrSetObjectNotExistsAsync(aParent, { 519 | type: 'channel', 520 | common: { 521 | name: 'Humidity', 522 | } 523 | }); 524 | 525 | await adapter.extendOrSetObjectNotExistsAsync(`${aParent}.Humidity`, { 526 | type: 'state', 527 | common: { 528 | name: 'Humidity', 529 | type: 'number', 530 | role: 'value.humidity', 531 | read: true, 532 | write: false, 533 | unit: '%' 534 | } 535 | }); 536 | 537 | await adapter.setStateAsync(`${aParent}.Humidity`, {val: aModule.dashboard_data.Humidity, ack: true}); 538 | } 539 | } 540 | 541 | async function handleNoise(aModule, aParent) { 542 | aParent += '.Noise'; 543 | 544 | if (!aModule.dashboard_data) { 545 | return; 546 | } 547 | 548 | if (aModule.dashboard_data.Noise !== undefined) { 549 | await adapter.extendOrSetObjectNotExistsAsync(aParent, { 550 | type: 'channel', 551 | common: { 552 | name: 'Noise', 553 | } 554 | }); 555 | 556 | await adapter.extendOrSetObjectNotExistsAsync(`${aParent}.Noise`, { 557 | type: 'state', 558 | common: { 559 | name: 'Noise', 560 | type: 'number', 561 | role: 'value', 562 | read: true, 563 | write: false, 564 | unit: 'dB' 565 | } 566 | }); 567 | 568 | await adapter.setStateAsync(`${aParent}.Noise`, {val: aModule.dashboard_data.Noise, ack: true}); 569 | } 570 | } 571 | 572 | async function handleHealthIdx(aModule, aParent) { 573 | aParent += '.HealthIdx'; 574 | 575 | if (!aModule.dashboard_data) { 576 | return; 577 | } 578 | 579 | if (aModule.dashboard_data.health_idx !== undefined) { 580 | await adapter.extendOrSetObjectNotExistsAsync(aParent, { 581 | type: 'channel', 582 | common: { 583 | name: 'Health index', 584 | } 585 | }); 586 | 587 | await adapter.extendOrSetObjectNotExistsAsync(`${aParent}.HealthIdx`, { 588 | type: 'state', 589 | common: { 590 | name: 'Health Index', 591 | type: 'number', 592 | role: 'value', 593 | read: true, 594 | write: false, 595 | } 596 | }); 597 | 598 | await adapter.setStateAsync(`${aParent}.HealthIdx`, {val: aModule.dashboard_data.health_idx, ack: true}); 599 | 600 | await adapter.extendOrSetObjectNotExistsAsync(`${aParent}.HealthIdxString`, { 601 | type: 'state', 602 | common: { 603 | name: 'Health Index (String)', 604 | type: 'string', 605 | states: {'0 = Healthy': '0 = Healthy', '1 = Fine': '1 = Fine', '2 = Fair': '2 = Fair', '3 = Poor': '3 = Poor', '4 = Unhealthy': '4 = Unhealthy', 'unknown IDX': 'unknown IDX'}, 606 | role: 'state', 607 | read: true, 608 | write: false, 609 | } 610 | }); 611 | 612 | let health_status = 'unknown'; 613 | switch (aModule.dashboard_data.health_idx) { 614 | case 0: 615 | health_status = '0 = Healthy'; 616 | break; 617 | case 1: 618 | health_status = '1 = Fine'; 619 | break; 620 | case 2: 621 | health_status = '2 = Fair'; 622 | break; 623 | case 3: 624 | health_status = '3 = Poor'; 625 | break; 626 | case 4: 627 | health_status = '4 = Unhealthy'; 628 | break; 629 | default: 630 | health_status = 'unknown IDX'; 631 | break; 632 | } 633 | 634 | await adapter.setStateAsync(`${aParent}.HealthIdxString`, {val: health_status, ack: true}); 635 | } 636 | } 637 | 638 | async function handlePlace(aModule, aParent) { 639 | aParent += '.Place'; 640 | 641 | if (!aModule.place) { 642 | return; 643 | } 644 | 645 | if (aModule.place.city !== undefined) { 646 | await adapter.extendOrSetObjectNotExistsAsync(aParent, { 647 | type: 'channel', 648 | common: { 649 | name: 'Place', 650 | } 651 | }); 652 | 653 | await adapter.extendOrSetObjectNotExistsAsync(`${aParent}.city`, { 654 | type: 'state', 655 | common: { 656 | name: 'city', 657 | type: 'string', 658 | role: 'location', 659 | read: true, 660 | write: false, 661 | } 662 | }); 663 | 664 | await adapter.setStateAsync(`${aParent}.city`, {val: aModule.place.city, ack: true}); 665 | } 666 | if (aModule.place.country !== undefined) { 667 | await adapter.extendOrSetObjectNotExistsAsync(`${aParent}.country`, { 668 | type: 'state', 669 | common: { 670 | name: 'country', 671 | type: 'string', 672 | role: 'location', 673 | read: true, 674 | write: false, 675 | } 676 | }); 677 | 678 | await adapter.setStateAsync(`${aParent}.country`, {val: aModule.place.country, ack: true}); 679 | } 680 | if (aModule.place.timezone !== undefined) { 681 | await adapter.extendOrSetObjectNotExistsAsync(`${aParent}.timezone`, { 682 | type: 'state', 683 | common: { 684 | name: 'timezone', 685 | type: 'string', 686 | role: 'state', 687 | read: true, 688 | write: false, 689 | } 690 | }); 691 | 692 | await adapter.setStateAsync(`${aParent}.timezone`, {val: aModule.place.timezone, ack: true}); 693 | } 694 | if (aModule.place.location !== undefined) { 695 | await adapter.extendOrSetObjectNotExistsAsync(`${aParent}.location`, { 696 | type: 'state', 697 | common: { 698 | name: 'location', 699 | type: 'string', 700 | role: 'value.gps', 701 | read: true, 702 | write: false, 703 | } 704 | }); 705 | await adapter.setStateAsync(`${aParent}.location`, {val: `${aModule.place.location[0]};${aModule.place.location[1]}`, ack: true}); 706 | } 707 | } 708 | 709 | async function handlePressure(aModule, aParent) { 710 | aParent += '.Pressure'; 711 | 712 | if (!aModule.dashboard_data) { 713 | return; 714 | } 715 | 716 | if (aModule.dashboard_data.Pressure !== undefined) { 717 | await adapter.extendOrSetObjectNotExistsAsync(aParent, { 718 | type: 'channel', 719 | common: { 720 | name: 'Pressure', 721 | } 722 | }); 723 | 724 | await adapter.extendOrSetObjectNotExistsAsync(`${aParent}.Pressure`, { 725 | type: 'state', 726 | common: { 727 | name: 'Pressure', 728 | type: 'number', 729 | role: 'value.pressure', 730 | read: true, 731 | write: false, 732 | unit: 'mbar' 733 | } 734 | }); 735 | 736 | await adapter.setStateAsync(`${aParent}.Pressure`, {val: aModule.dashboard_data.Pressure, ack: true}); 737 | } 738 | 739 | if (aModule.dashboard_data.AbsolutePressure !== undefined) { 740 | await adapter.extendOrSetObjectNotExistsAsync(`${aParent}.AbsolutePressure`, { 741 | type: 'state', 742 | common: { 743 | name: 'Absolute pressure', 744 | type: 'number', 745 | role: 'value.pressure', 746 | read: true, 747 | write: false, 748 | unit: 'mbar' 749 | } 750 | }); 751 | 752 | await adapter.setStateAsync(`${aParent}.AbsolutePressure`, {val: aModule.dashboard_data.AbsolutePressure, ack: true}); 753 | } 754 | 755 | if (aModule.dashboard_data.pressure_trend !== undefined) { 756 | await adapter.extendOrSetObjectNotExistsAsync(`${aParent}.PressureTrend`, { 757 | type: 'state', 758 | common: { 759 | name: 'Pressure trend', 760 | type: 'string', 761 | role: 'value.direction', 762 | read: true, 763 | write: false 764 | } 765 | }); 766 | 767 | await adapter.setStateAsync(`${aParent}.PressureTrend`, {val: aModule.dashboard_data.pressure_trend, ack: true}); 768 | } 769 | } 770 | }; 771 | --------------------------------------------------------------------------------