{ "version": "2.0", "xmlns": { "atom": "http://www.w3.org/2005/Atom", "content": "http://purl.org/rss/1.0/modules/content/", "georss": "http://www.georss.org/georss", "gml": "http://www.opengis.net/gml" }, "channel": { "title": "fboës - Der Blog | Startseite", "link": "https://journal.3960.org/", "description": "Programmierung, Luft- & Raumfahrt, Kurioses: Der Blog von und mit Frank Boës.", "language": "de-DE", "copyright": "© 2008-2023 Creative Commons BY", "atom_link": { "href": "https://journal.3960.org/rss.json", "rel": "self", "type": "application/rss+json" }, "lastBuildDate": "Sun, 03 Mar 2024 19:00:07 +0100", "atom_updated": "2024-03-03T19:00:07+01:00", "generator": "blogophon", "image": { "url": "https://cdn.3960.org/images/tile-128x128.png", "title": "fboës - Der Blog", "link": "https://journal.3960.org/" }, "items": [ { "title": "Simulatoren mit einem Gamepad steuern?", "description": "
Der Einstieg in den Bereich Flugsimulation kann schon sehr abschreckend sein – nicht zuletzt wegen den Investitionen in eine vernünftige Hardware. Ein Joystick mit Schubkontrolle ist ja das absolute Minimum, was man an Eingabegeräten für den PC besitzen sollte…
\n…aber ist das wirklich so?
", "content_encoded": "Der Einstieg in den Bereich Flugsimulation kann schon sehr abschreckend sein – nicht zuletzt wegen den Investitionen in eine vernünftige Hardware. Ein Joystick mit Schubkontrolle ist ja das absolute Minimum, was man an Eingabegeräten für den PC besitzen sollte…
\n…aber ist das wirklich so?
\n\nDenn tatsächlich hat Tuuvas ein Projekt begonnen, um für so ziemlich jedes Flugzeug im Digital Combat Simulator (DCS) mit einem Gamepad steuern zu können.
\n\nDas eigentlich Interessante an seinem Erklärungsvideo ist, dass man mittels Steam einem Gamepad neue Funktionen zuweisen kann, so dass jeder Simulator auf einem PC mit einem Gamepad gesteuert werden könnte.
\nSeine besonderen Tricks mittels Steam sind:
\nTuuvas' Vorschläge für ein Setup sehen grundsätzlich ähnlich aus. Vereinfacht könnte man für jeden Simulator folgende Ideen verwenden.
\nGamepad Eingabe | \nSimulator Ausgabe | \n
---|---|
Linkes analoges Steuerkreuz | \nJoystick X/Y | \n
Rechtes analoges Steuerkreuz | \nMauszeiger bewegen | \n
Schultertasten | \nLinker / rechter Mausklick | \n
Schulter-Trigger | \nRuder l/r | \n
Mittlere zwei Buttons | \nRadbremse l/r | \n
D-Pad | \nTrimmung | \n
Buttons | \nSchub und weitere Funktionen | \n
Durch die Verwendung der Maus ist in Simulatoren mit klickbarem Cockpit so ziemlich jede andere Funktion bedienbar, in dem man mit dem Mauszeiger einfach die Knöpfe im Cockpit anklickt.
\nKein Joystick, aber ein Gamepad daheim? Steam erlaubt es euch, euer Gamepad in ein HOTAS zu verwandeln, mit dem ihr ganz passabel Flugsimulationen steuern könnt.
\nBTW: Die Idee mit dem HOTAS im Gamepad-Format wurde auch mit dem Yawman Arrow umgesetzt – kostet dann aber auch wiederum deutlich mehr als ein reguläres Gamepad.
", "link": "https://journal.3960.org/posts/2024-03-03-simulatoren-mit-gamepad-steuern/", "pubDate": "Sun, 03 Mar 2024 18:55:24 +0100", "atom_published": "2024-03-03T18:55:24+01:00", "atom_updated": "2024-03-03T18:55:24+01:00", "guid": "user/posts/2024-03-03-simulatoren-mit-gamepad-steuern/index.md", "author": "info@3960.org (Frank Boës)", "categories": [ "Simulation", "Joystick", "Fliegerei", "Spiel" ] }, { "title": "Marcel Reif – „Sei ein Mensch!“", "description": "\nMarcel Reifs Rede bei der Gedenkstunde für die Opfer des Nationalsozialismus im Bundestag am 31.01.2024.
", "content_encoded": "\nMarcel Reifs Rede bei der Gedenkstunde für die Opfer des Nationalsozialismus im Bundestag am 31.01.2024.
", "link": "https://journal.3960.org/posts/2024-02-12-marcel-reif-sei-mensch/", "pubDate": "Mon, 12 Feb 2024 19:11:27 +0100", "atom_published": "2024-02-12T19:11:27+01:00", "atom_updated": "2024-02-12T19:11:27+01:00", "guid": "user/posts/2024-02-12-marcel-reif-sei-mensch/index.md", "author": "info@3960.org (Frank Boës)", "categories": [ "Philosophie" ] }, { "title": "The Insane Engineering of the SR-71 Blackbird", "description": "The Lockheed SR-71 Blackbird is a truly unique aircraft. This great video highlights the stranger aspects of hypersonic flight and the engineering obstacles which needed to be overcome to make the SR-71 reach speeds in excess of Mach 3 in 85,000 ft.
\n\nAs of today, its successor the Lockheed Martin SR-72 „Son of Blackbird“ has not yet taken flight – even though in cinemas and the Microsoft Flight Simulator the Lockheed Martin Darkstar has surpassed Mach 10 at least once. 😉
", "content_encoded": "The Lockheed SR-71 Blackbird is a truly unique aircraft. This great video highlights the stranger aspects of hypersonic flight and the engineering obstacles which needed to be overcome to make the SR-71 reach speeds in excess of Mach 3 in 85,000 ft.
\n\nAs of today, its successor the Lockheed Martin SR-72 „Son of Blackbird“ has not yet taken flight – even though in cinemas and the Microsoft Flight Simulator the Lockheed Martin Darkstar has surpassed Mach 10 at least once. 😉
", "link": "https://journal.3960.org/posts/2024-02-05-insane-engineering-the-sr-71-blackbird/", "pubDate": "Mon, 05 Feb 2024 18:24:10 +0100", "atom_published": "2024-02-05T18:24:10+01:00", "atom_updated": "2024-02-05T18:24:10+01:00", "guid": "user/posts/2024-02-05-insane-engineering-the-sr-71-blackbird/index.md", "author": "info@3960.org (Frank Boës)", "categories": [ "Fliegerei", "Militär", "Raumfahrt", "Technologie" ] }, { "title": "Der kleinste JavaScript-Unit-Tester der Welt", "description": "Schon das kleinste JavaScript-Projekt kann von Unit Testing profitieren. Aber gerade in kleinen Projekten können Lösungen wie Mocha oder Jest sich als viel zu schwergewichtig anfühlen.
\nAber warum eigentlich ein Framework verwenden? Wozu das Projekt mit Dependencies vollstopfen, wenn ein paar elegante Zeilen Code für unsere Zwecke reichen?
", "content_encoded": "Schon das kleinste JavaScript-Projekt kann von Unit Testing profitieren. Aber gerade in kleinen Projekten können Lösungen wie Mocha oder Jest sich als viel zu schwergewichtig anfühlen.
\nAber warum eigentlich ein Framework verwenden? Wozu das Projekt mit Dependencies vollstopfen, wenn ein paar elegante Zeilen Code für unsere Zwecke reichen?
\n\nIn seiner einfachsten Form ist Unit Testing nichts anderes als das Vergleichen von Annahmen mit der Ausgabe von tatsächlichen Code. In JavaScript gibt es dafür eine eingebaute Lösung: console.assert()
.
const starDestroyer = new StarDestroyer();\nconsole.assert(starDestroyer !== null, 'Star destroyer exists');\nconsole.assert(starDestroyer.speed >= 0, 'Star destroyer has speed property with positive Number');\nconsole.assert(starDestroyer.faction === 'imperial', 'Star destroyer is always imperial');\n
\nIm Browser wie auch in Node.js erzeugt diese Methode eine Konsolen-Ausgabe, wenn die Annahme nicht zutrifft.
\nWenn in ein und derselben Datei mehrere Testfälle geprüft werden sollen, ist eine klares Scoping von Variablen notwendig. So wird sichergestellt, das Ergebnisse aus einem Test nicht irrtümlicherweise im nächsten Test verwendet werden. In JavaScript ist die Erzeugung eines neuen Scopes jederzeit mittels {…}
möglich:
{\n const starDestroyer = new StarDestroyer();\n console.assert(starDestroyer !== null, 'Star destroyer exists');\n console.assert(starDestroyer.speed >= 0, 'Star destroyer has speed property with positive Number');\n console.assert(starDestroyer.faction === 'imperial', 'Star destroyer is always imperial');\n}\n\n// No starDestroyer here\n
\nMehrere Tests können so in einer Datei abgehandelt werden.
\nFür automatisiertes Testing ist die Verwendung von console.assert
natürlich zu kurz gesprungen, denn hier kann nur durch Sichtkontrolle überprüft werden, ob alle Tests sauber durchgelaufen sind. Wichtig wäre es also, auf der Kommandozeile einen Exit-Code zu bekommen, der auf einen Fehler hinweist.
Assert
in Node.jsIn Node.js existiert dafür Assert
:
const assert = require('node:assert');\n\n{\n const starDestroyer = new StarDestroyer();\n assert.ok(starDestroyer !== null, 'Star destroyer exists');\n assert.ok(starDestroyer.speed >= 0, 'Star destroyer has speed property with positive Number');\n assert.strictEqual(starDestroyer.faction, 'imperial', 'Star destroyer is always imperial');\n}\n
\nIm Fehlerfall wird assert.AssertionError
geworfen, der auf der CLI einen auch für Automatisierungen wahrnehmbaren Exit-Code erzeugt.
Der Nachteil dieser Lösung ist gut erkennbar: Diese Art von Testing funktioniert nur in Node.js, aber nicht im Browser.
\nGott sei Dank ist die Idee mit dem Werfen von Errors aber gar nicht so komplex. Und damit können wir uns eine kleine Funktion konstruieren, mit der wir ganz grundsätzliches Testing durchführen können:
\nconst assertOk = (assertion, message = 'Assertion') => {\n console.log((assertion ? "✅" : "💥") + " " + message);\n if (!assertion) {\n throw new Error(`"${message}" failed`);\n }\n};\n
\nDamit schlagen wir mehrere Fliegen mit einer Klappe:
\nEtwas aussagekräftiger könnte die Funktion sogar so aussehen:
\nconst assertStrictEqual = (a, b = true, message = '') => {\n message === '' || (message += " | ");\n message += message = `'${a}' equals '${b}'`;\n\n console.log((a === b ? "✅" : "💥") + " " + message);\n if (a !== b) {\n throw new Error(`"${message}" failed`);\n }\n};\n
\nDamit können nicht nur direkt Vergleiche durchgeführt werden, sondern auch gleich eine aussagekräftige Nachricht über den tatsächlichen Inhalt des Tests mit ausgegeben werden.
\nWenn ihr ein Drop-In-Replacement für assert
von Node.js haben wollt, könnt ihr die beiden Funktionen in statische Methoden verwandeln:
const assert = {\n /**\n * Check if `assertion` is `true`. Throw Error on error.\n * @param {Boolean} assertion\n * @param {String} message\n */\n ok(assertion, message = 'Assertion') {\n console.log((assertion ? "✅" : "💥") + " " + message);\n if (!assertion) {\n throw new Error(`"${message}" failed`);\n }\n },\n\n /**\n * Check if `actual` strictly equals `b`. Throw Error on error.\n * @param {any} actual\n * @param {any} expected\n * @param {String} message\n */\n strictEqual(actual, expected, message = '') {\n message === '' || (message += " | ");\n message += message = `'${actual}' equals '${expected}'`;\n\n assert.ok(actual == expected, message)\n }\n};\n
\nOh, und wenn ihr eure Tests noch gruppieren wollt:
\nconsole.group('Testing StarDestroyer');\n{\n const starDestroyer = new StarDestroyer();\n assert.ok(starDestroyer !== null, 'Star destroyer exists');\n assert.ok(starDestroyer.speed >= 0, 'Star destroyer has speed property with positive Number');\n assert.strictEqual(starDestroyer.faction, 'imperial', 'Star destroyer is always imperial');\n}\nconsole.groupEnd();\n
\n…erzeugt eine schön gruppierte Ausgabe eurer Tests.
\nVoilà! Eine Beispiel-Ausgabe:
\nGeoJson.Feature\n ✅ Type matches | 'Feature' equals 'Feature'\n ✅ Feature.properties.title | 'Test' equals 'Test'\n ✅ Feature.geometry.type | 'Point' equals 'Point'\n ✅ No id\nGeoJson.FeatureCollection\n ✅ 'FeatureCollection' equals 'FeatureCollection'\n ✅ Bounding Box West | '6.5664576' equals '6.5664576'\n ✅ Bounding Box South | '51.04571' equals '51.04571'\n ✅ Bounding Box East | '1.53946' equals '1.53946'\n ✅ Bounding Box North | '58.109285' equals '58.109285'\n ✅ FeatureCollection | 'FeatureCollection' equals 'FeatureCollection'\n ✅ Two Features exist | '2' equals '2'\n ✅ Feature.type | 'Feature' equals 'Feature'\n ✅ Feature.properties.title | 'Sailing boat' equals 'Sailing boat'\n
\nNatürlich ersetzt diese sehr simple Herangehensweise nicht wirklich echte Unit-Test-Frameworks. So ist zum Beispiel der Vergleich von Objekt-Strukturen nicht möglich, das Testen von Exceptions oder auch die Verwendung von Mocking und ähnlichen Features. Aber zumindest senkt es die Hürde, überhaupt mit Testing zu beginnen. 😉
", "link": "https://journal.3960.org/posts/2024-02-03-kleinste-javascript-unit-tester-welt/", "pubDate": "Sat, 03 Feb 2024 18:14:32 +0100", "atom_published": "2024-02-03T18:14:32+01:00", "atom_updated": "2024-02-04T05:06:25+01:00", "guid": "user/posts/2024-02-03-kleinste-javascript-unit-tester-welt/index.md", "author": "info@3960.org (Frank Boës)", "categories": [ "Javascript", "Programmierung", "Webdevelop" ] }, { "title": "Wie funktioniert der Tolino-Webbrowser?", "description": "In den eBook-Readern von Tolino steckt auch ein Webbrowser. Welche Fähigkeiten hat der eigentlich? Und lohnt es sich, eigens für eBook-Reader optimierte Webseiten zu bauen?
", "content_encoded": "In den eBook-Readern von Tolino steckt auch ein Webbrowser. Welche Fähigkeiten hat der eigentlich? Und lohnt es sich, eigens für eBook-Reader optimierte Webseiten zu bauen?
\n\nJeder Browser strahlt seine eigene Browser-Kennung ab: Den User Agent String. Er verrät nicht nur, welche Browser-Software in welcher Version im Einsatz ist, sondern auch meist das Betriebssystem.
\nFür einen Tolino Shine sieht das wie folgt aus:
\nMozilla/5.0 (Linux; Android 8.1.0; de-; tolino shine 4/16.1.0) AppleWebKit/537.36 (KHTML, like Gecko) Verions/4.0 Chrome/61.0.0.0 Mobile Mobile Safari/537.36\n
\nDamit lüften sich mehrere Geheimnisse: Nicht nur ist auf dem Tolino ein Android 8 als Betriebssystem im Einsatz, sondern der Browser ist auch ein veralteter Google Chrome 61. (Zum Zeitpunkt der Untersuchung war gerade der Google Chrome 121 aktuell.) Damit beherrscht er leider nicht alle modernen Tricks von CSS und ECMAScript, ist aber für Brot-und-Butter-Websites immer noch eine sehr gute Wahl.
\nMein Tolino Webbrowser hat eine interessante Eigenheit: Er strahlt an jeden besuchten Web-Server einen eigenen Header ab:
\nX-REQUESTED-WITH: de.telekom.epub\n
\nTatsächlich gibt es möglicherweise historische Gründe für diesen Header, uns hilft dies aber sonst nicht weiter. Viel spannender ist für uns…
\nDie physische Bildschirmgröße des Tolino Shine 4 ist 1072×1448 Pixel. Im Web wird dabei ein Device Pixel Ratio von 1.875 angenommen.
\nNun haben wir auf einem eBook-Reader zwei Herausforderungen, die bei der Gestaltung von Websites berücksichtigt werden müssen:
\nSinnvoll wäre also eine Reaktion des Webbrowsers auf folgende Media Queries:
\n\nUnd hier kommt der etwas traurige Part: In Bezug auf Media Queries gibt es leider keine Anpassungen des Google Chrome auf dem Tolino gegenüber einem normalen Google Chrome auf einem Android-Smartphone oder -Tablet:
\nMedia Query | \nValue | \n
---|---|
Media type | \nscreen | \n
Monochrome | \nfalse | \n
Color | \ntrue | \n
Width | \n572px | \n
Height | \n773px | \n
Resolution (dpi) | \n180 | \n
Colors | \n8 | \n
Orientation | \nportrait | \n
Script | \nfalse | \n
Hover | \nfalse | \n
Pointer | \ncoarse | \n
Prefers reduced motion | \nfalse | \n
Kurz gesprochen gibt es also keine Media Query, mit der das CSS einer Website für den Webbrowser des Tolino zugeschnitten werden kann.
\nMeine Lösung für dieses Dilemma ist wenig schön, aber besser als nichts:
\n(function () {\n if (window.navigator.userAgent.match(/(Tolino|Kindle)/i)) {\n document.body.classList.add("is-ebook-reader");\n }\n})();\n
\nAuf jeder neuen Seite wird mit einer kleinen Zeile JavaScript kurz überprüft, ob es im User Agent String einen Hinweis darauf gibt, dass es sich bei dem besuchenden Browser um einen eBook-Reader handelt. Wenn ja, wird der Seite eine CSS-Klasse is-ebook-reader
hinzugefügt.
Zum Spaß habe ich das hier in diesen Blog eingebaut, und einfach mittels CSS Custom Properties eine kleine Fallunterscheidung in mein CSS gebaut. Eine ganz einfache Lösung könnte aber wie folgt aussehen:
\n:root {\n --color-background: #eee;\n --color-text: #112;\n --color-link: orange;\n --text-decoration-link: none\n}\n\n.is-ebook-reader {\n --color-background: white;\n --color-text: black;\n --color-link: black;\n --text-decoration-link: underline dotted;\n}\n\na {\n color: var(--color-link);\n text-decoration: var(--text-decoration-link);\n}\n
",
"link": "https://journal.3960.org/posts/2023-12-29-wie-funktioniert-tolino-webbrowser/",
"pubDate": "Fri, 29 Dec 2023 18:14:24 +0100",
"atom_published": "2023-12-29T18:14:24+01:00",
"atom_updated": "2023-12-30T19:47:58+01:00",
"guid": "user/posts/2023-12-29-wie-funktioniert-tolino-webbrowser/index.md",
"author": "info@3960.org (Frank Boës)",
"categories": [
"CSS",
"Programmierung",
"Webdevelop"
]
},
{
"title": "One revolution per minute",
"description": "\nManche Künstler trifft man mehrfach im Leben. Erik Wernquist ist so ein Künstler. „One revolution per minute“ zeigt uns das Leben an Bord eines Kreuzfahrtraumschiffs…
", "content_encoded": "\nManche Künstler trifft man mehrfach im Leben. Erik Wernquist ist so ein Künstler. „One revolution per minute“ zeigt uns das Leben an Bord eines Kreuzfahrtraumschiffs…
", "link": "https://journal.3960.org/posts/2023-12-19-one-revolution-per-minute/", "pubDate": "Tue, 19 Dec 2023 18:43:57 +0100", "atom_published": "2023-12-19T18:43:57+01:00", "atom_updated": "2023-12-19T18:43:57+01:00", "guid": "user/posts/2023-12-19-one-revolution-per-minute/index.md", "author": "info@3960.org (Frank Boës)", "categories": [ "The Cool", "Raumfahrt", "Technologie" ] }, { "title": "RIDON Hoverbike", "description": "\nDie Portfolio-Seite von Ivan Tantsiura ist eine wunderbare Fundgrube. Tantsiura arbeitet als Principal Concept Artist bei Crytek, und hat Unmengen an futuristischen Designs geschaffen – unter anderem das RIDON Hoverbike.
\nVerglichen mit dem Speederbike aus Star Wars sieht man beim RIDON Hoverbike, warum das Hoverbike schwebt – und auch, dass diese Art von Schweben schon etwas sehr dynamisches hat. Der Mechanismus (wie auch die Welt des Hoverbikes) erinnern an Simon Stålenhag und seine schwebenden Schiffe.
\nMan bekommt direkt Lust, selber eine kleine Spritztour mit dem RIDON Hoverbike zu machen. Wo bleibt also der Arcade-Automat beziehungsweise das VR-Spiel zum Thema?
", "content_encoded": "\nDie Portfolio-Seite von Ivan Tantsiura ist eine wunderbare Fundgrube. Tantsiura arbeitet als Principal Concept Artist bei Crytek, und hat Unmengen an futuristischen Designs geschaffen – unter anderem das RIDON Hoverbike.
\nVerglichen mit dem Speederbike aus Star Wars sieht man beim RIDON Hoverbike, warum das Hoverbike schwebt – und auch, dass diese Art von Schweben schon etwas sehr dynamisches hat. Der Mechanismus (wie auch die Welt des Hoverbikes) erinnern an Simon Stålenhag und seine schwebenden Schiffe.
\nMan bekommt direkt Lust, selber eine kleine Spritztour mit dem RIDON Hoverbike zu machen. Wo bleibt also der Arcade-Automat beziehungsweise das VR-Spiel zum Thema?
", "link": "https://journal.3960.org/posts/2023-12-13-ridon-hoverbike/", "pubDate": "Wed, 13 Dec 2023 18:38:16 +0100", "atom_published": "2023-12-13T18:38:16+01:00", "atom_updated": "2023-12-14T12:44:05+01:00", "guid": "user/posts/2023-12-13-ridon-hoverbike/index.md", "author": "info@3960.org (Frank Boës)", "categories": [ "The Cool", "Raumfahrt", "Technologie", "Geckobar" ] }, { "title": "Sommermodus mit FRITZ!DECT und Home Assistant", "description": "Der Sommermodus für die FRITZ!DECT-Thermostate wird über die FRITZ!Box gesteuert – mit einem festen Zeitplan. Mehr Flexibilität gibt es mit dem Home Assistant, um zum Beispiel auf Temperaturdaten oder Wetterprognosen reagieren zu können.
", "content_encoded": "Der Sommermodus für die FRITZ!DECT-Thermostate wird über die FRITZ!Box gesteuert – mit einem festen Zeitplan. Mehr Flexibilität gibt es mit dem Home Assistant, um zum Beispiel auf Temperaturdaten oder Wetterprognosen reagieren zu können.
\n\nDummerweise lässt sich der Sommermodus an den Thermostaten vom Home Assistant aus nicht einfach auslösen. Und selbst die im Home Assistant vorhandene Möglichkeit, den Thermostat einfach auszuschalten, funktioniert nicht wirklich gut – der nächste Schaltbefehl an den Thermostat hebt die Abschaltung wieder auf. Außerdem kann der Sommermodus deutlich mehr als nur die Heizung abzuschalten – unter anderem kümmert er sich um das gelegentliche Bewegen der Ventile, um sie vor dem Steckenbleiben zu bewahren.
\n…ist zweiteilig: In der FRITZ!Box legen wir mittels Vorlagen das ein- und ausschalten des Sommermodus' an – und im Home Assistant lösen wir dann die Vorlagen aus.
\n\n
Den Druck auf einen dieser Knöpfe könnt ihr im Hone Assistant abfangen und zusätzliche Aktionen auslösen.
\nDazu müsst ihr eine Automatisierung bauen, die beim Druck auf die Knöpfe neben den Vorlagen in der FRITZ!Box noch zusätzliche Aktionen auslösen:
\nalias: "Heizungen: Sommermodus an"\ndescription: ""\ntrigger:\n - platform: device\n device_id: 5f6c4925f76d4ef27407160a968bb7fd\n domain: button\n entity_id: button.heizungen_sommermodus_an\n type: pressed\ncondition: []\naction:\n - service: notify.notify\n data:\n title: "Sommermodus an"\n message: >-\n Alle Heizungen sind nun abgeschaltet, willkommen im Sommermodus.\n enabled: true\nmode: single\n
\nWir haben aber ja die ganze Arbeit nicht auf uns genommen, um im Home Assistant selber auf Knöpfe drücken zu müssen. Tatsächlich können wir Automatisierungen des Home Assistants für uns auf den Knopf drücken lassen.
\nDabei kann in jeder Automatisierung als Aktion der Druck auf die Knöpfe ausgelöst werden:
\naction:\n - service: button.press\n data: {}\n target:\n entity_id: button.sommermodus_an\n enabled: true\n
\nSomit könnt ihr euch nun beliebige Auslöser im Home Assistant konfigurieren, die eure Heizung in den Sommermodus schicken.
\nDie selbe Methode lässt sich neben dem Sommermodus auch für den Urlaubsmodus und eigentlich jede andere Vorlage in der FRITZ!Box anwenden. Dafür müsst ihr nur jeweils in der FRITZ!Box das gewünschte Verhalten als Vorlage definieren, um danach den zugehörigen Button im Home Assistant drücken zu können – beziehungsweise durch eine Automatisierungen auslösen zu können.
\nTheoretisch könnt ihr darüber auch abweichende Zeitpläne oder geänderte Spar-/Komforttemperaturen oder zeitlich begrenzte EInstellungen wie Boost oder Fenster-Modus aktivieren.
", "link": "https://journal.3960.org/posts/2023-05-26-sommermodus-mit-fritz-dect-home-assistant/", "pubDate": "Fri, 26 May 2023 18:45:26 +0200", "atom_published": "2023-05-26T18:45:26+02:00", "atom_updated": "2023-05-29T09:48:19+02:00", "guid": "user/posts/2023-05-26-sommermodus-mit-fritz-dect-home-assistant/index.md", "author": "info@3960.org (Frank Boës)", "categories": [ "Home Assistant", "Programmierung", "Technologie", "The Cool", "AVM", "Homeoffice" ] }, { "title": "„War at Sea“ – Tabletop-Regeln", "description": "Das Miniaturenspiel „War at Sea“ aus der Axis & Allies-Serie hatte es Bruder Axel und mir sehr angetan. Dummerweise wird das Spiel augenscheinlich nicht mehr hergestellt. Aber das Internet steckt voller Möglichkeiten.
", "content_encoded": "Das Miniaturenspiel „War at Sea“ aus der Axis & Allies-Serie hatte es Bruder Axel und mir sehr angetan. Dummerweise wird das Spiel augenscheinlich nicht mehr hergestellt. Aber das Internet steckt voller Möglichkeiten.
\n\nWas benötigen wir, um das Spiel wieder auferstehen zu lassen?
\nSo verrät uns der Wikipedia-Eintrag zu „War at Sea“ nicht nur, dass zuletzt um die 280 verschiedene Einheiten für das Spiel verfügbar waren, sondern verlinkt auch die vom Hersteller frei zugänglich gemachten Spielregeln von „War at Sea“ – wenn auch diese inzwischen nur noch über web.archive.org erreichbar sind. Die Spielregeln erklären nicht nur das Spiel, sondern zeigen auch, welche weiteren Teile ihr für das Spiel benötigt.
\nWenn ihr die dazu die „War at Sea“-Tabletop-Spielregeln benutzt, braucht ihr auch kein Spielfeld, sondern könnt das Spiel auf einer beliebigen Oberfläche (wie zum Beispiel dem Wohnzimmerfußboden) spielen.
\nAußerdem gibt es im Internet frei verfügbare Datenbanken über alle in „War at Sea“ verfügbaren Einheiten und sogar die Designs der Sammelkarten. Der Charme der ursprünglichen Sammelkarten kommt damit zwar nicht auf, aber dafür hat man Zugriff auf alle Einheiten.
\nJetzt fehlen nur noch die Spielfiguren. Eigentlich könnte man hier ja alles nehmen, was in irgendeiner Form ein Schiff (beziehungsweise fliegende Einheiten) darstellt. Mit passenden Schiffsminiaturen kommt man aber deutlich näher an das ursprüngliche Spielgefühl.
\nDazu muss man nur wissen, wie groß die Spielfiguren damals waren:
\n\nDie 266 Meter lange USS Essex misst 148 mm. […] Der Maßstab scheint mir 1:1800 zu sein.
Da dieser Maßstab nicht unbedingt üblich ist, könnte man sich hier mit 3d-gedruckten Schiffen behelfen.
\n\nBei Thingiverse gibt es ausgezeichnete Schiffs-Miniaturen für „War at Sea“, die ihr nur noch mit einem 3d-Drucker drucken lassen müsst – und schon könnt ihr euer eigenes Unternehmen Rheinübung ausprobieren.
\n", "link": "https://journal.3960.org/posts/2023-05-01-war-at-sea-tabletop-regeln/", "pubDate": "Mon, 01 May 2023 18:56:18 +0200", "atom_published": "2023-05-01T18:56:18+02:00", "atom_updated": "2023-05-25T08:52:37+02:00", "guid": "user/posts/2023-05-01-war-at-sea-tabletop-regeln/index.md", "author": "info@3960.org (Frank Boës)", "categories": [ "Brettspiel", "Idee", "Militär" ] }, { "title": "Verabschiedung", "description": "", "content_encoded": "", "link": "https://journal.3960.org/posts/2023-04-10-verabschiedung/", "pubDate": "Mon, 10 Apr 2023 18:54:53 +0200", "atom_published": "2023-04-10T18:54:53+02:00", "atom_updated": "2023-04-10T18:54:53+02:00", "guid": "user/posts/2023-04-10-verabschiedung/index.md", "author": "info@3960.org (Frank Boës)", "categories": [ "Programmierung", "Philosophie", "Webdevelop" ] }, { "title": "Die 24h-Uhr – als Web Component", "description": "Web Components zu bauen ist gar nicht so kompliziert, selbst ohne Framework. Nach meinen vorherigen Überlegungen zur Verwendung von SVGs in Web Components war es also höchste Zeit, eine 24-Stunden-Uhr zu bauen.
", "content_encoded": "Web Components zu bauen ist gar nicht so kompliziert, selbst ohne Framework. Nach meinen vorherigen Überlegungen zur Verwendung von SVGs in Web Components war es also höchste Zeit, eine 24-Stunden-Uhr zu bauen.
\n\n24h-Uhren sind analoge Uhren, die einen feinen Unterschied zu herkömmlichen Uhren aufweisen. An Stelle von 12 Stunden pro Umdrehung des Stundenzeigers zeigen sie 24 Stunden an. Damit braucht also der Stundenzeiger einen kompletten Tag für einen Vollkreis.
\nDamit ist es möglich, die vierundzwanzig Stunden auf dem Ziffernblatt grafisch in Tag, Dämmerung und Nacht einzuteilen. Da diese sich in Bezug auf das Datum und die geografische Position verändern, brauchte die Uhr also auch diese Informationen. In meinem Fall habe ich eine Skala auf dem Stundenring angebracht, die sich für die Nacht dunkelblau, für die Dämmung mittelblau und für den Tag hellblau verfärbt.
\n\n
Der Bau als Web Component erlaubt es nun, die Uhr mit relativ wenig HTML in eine beliebige Seite einzubauen. Unter der Github-Seite zur 24h-Uhr findet sich eine knappe Anleitung, der Einbau einer Web Component muss aber nicht komplizierter sein als:
\n<twentyfour-hours-clock></twentyfour-hours-clock>\n
\nWeitere Attribute erlauben es, die Component vorher mit Werten zu bestücken – die sich auch im Nachhinein mit JavaScript ändern lassen:
\n<twentyfour-hours-clock width="128" height="128" datetime="2011-10-10T14:48:00" longitude="auto" latitude="auto"></twentyfour-hours-clock>\n
\nAuch das Einfärben, Vergrößern und Verkleinern von Einzelteilen der Web Component sind mit CSS Custom Properties möglich:
\n<style>\ntwentyfour-hours-clock {\n --color-watchhand: pink;\n --color-night: #111111;\n}\n</style>\n<twentyfour-hours-clock></twentyfour-hours-clock>\n
\nDie Web Component selber wurde mit TypeScript gebaut. Tatsächlich wäre auch der Bau direkt in Vanilla JavaScript möglich gewesen. Meine letzten Abenteuer in TypeScript sind aber so angenehme Erfahrungen gewesen, dass ich die Unterstützung bei der Typisierung in JavaScript nicht mehr missen möchte, und die Web Component in TypeScript gebaut habe. Inzwischen halte ich bei JavaScript die Verwendung von TypeScript für so hilfreich, dass ich auf ESLint zur Überprüfung meiner Programmierung bei Privatprojekten gerne verzichte.
\nEin funktionierendes Beispiel für die fertige Uhr findet sich auf der Demonstrationsseite für die 24h-Uhr.
", "link": "https://journal.3960.org/posts/2023-03-05-24h-uhr-als-web-component/", "pubDate": "Sun, 05 Mar 2023 18:56:12 +0100", "atom_published": "2023-03-05T18:56:12+01:00", "atom_updated": "2023-03-05T18:56:12+01:00", "guid": "user/posts/2023-03-05-24h-uhr-als-web-component/index.md", "author": "info@3960.org (Frank Boës)", "categories": [ "Webdevelop", "Web-Components", "SVG", "Javascript", "CSS", "Geografie", "Outdoor", "Programmierung", "Technologie" ] }, { "title": "Pragmatische Heizungssteuerung im Home-Office", "description": "Mein neuestes Abenteuer mit dem Home Assistant in Verbindung mit FRITZ!DECT 301 beschäftigt sich mit einer smarten Heizungssteuerung für das Home-Office.
", "content_encoded": "Mein neuestes Abenteuer mit dem Home Assistant in Verbindung mit FRITZ!DECT 301 beschäftigt sich mit einer smarten Heizungssteuerung für das Home-Office.
\n\nAch, was habe ich alles gebaut und gebastelt. Los ging es mit einem einfachen Zeitplan in der FRITZ!Box, der jeden Montag bis Freitag in der Arbeitszeit die Heizung ein- und danach wieder ausschaltete. Ich war's zufrieden.
\nAber an Feiertagen, die zwischen Montag und Freitag lagen, lief die Heizung immer noch. Höchste Zeit, den Werktags-Sensor über den Home Assistant zu verbauen und in die Heizungssteuerung mit einzubeziehen. Dafür musste ich dann den Zeitplan aus der FRITZ!Box entfernen und ebenfalls in den Home Assistant übertragen.
\nUnd im Urlaub? Urlaube sind ja keine Feiertage. Also noch einen Kalender in den Home Assistant integriert, der an Urlaubstagen die Heizung ebenfalls ausgeschaltet hält.
\nOha, Gleitzeit und Überstunden: Mal starte ich früher, mal höre ich später auf. Also diesen Teil von Hand steuern? Und wie ist es am Wochenende, wenn man im Arbeitszimmer am privaten PC etwas bastelt?
\nWie man es auch dreht und wendet – der Plan hatte inzwischen viel zu viele Variablen bekommen. Eigentlich wollte ich doch nur, dass die Heizung läuft, wenn ich im Raum bin! Aber wie kann ich das dem Home Assistant beibringen?
\nGanz einfach! Ob im Büro jemand arbeitet kann man daran erkennen, dass einer der PCs im Büro eingeschaltet ist. Ein super-simples Rezept kann also prüfen, ob mindestens einer der Rechner dort an und im Netzwerk sichtbar ist, und schaltet entsprechend die Heizung an. Wenn keiner der Rechner im Netzwerk ist, wird die Heizung entsprechend ausgeschaltet.
\nDazu bauen wir uns einen Zustand, der eingeschaltet ist, wenn irgendeiner der PCs angeschaltet und im Netzwerk ist – und erst ausgeschaltet ist, wenn alle PCs ausgeschaltet sind. Das funktioniert wie folgt:
\narbeitspferd
und daddelkiste
.device_tracker
beginnt. In meinem Beispiel gehen wir von zwei PC-Trackern aus, device_tracker.arbeitspferd
und device_tracker.daddelkiste
.configuration.yaml
nun so erweitert werden, dass die beiden Tracker (oder beliebig viele Tracker) zu einem Sammel-Tracker zusammengefasst werden.- name: "Home-Office PCs"\n unique_id: device_tracker_homeoffice\n state: "{{ is_state('device_tracker.arbeitspferd', 'home') or is_state('device_tracker.daddelkiste', 'home') }}"\n icon: "mdi:lan-connect"\n device_class: presence\n
\nNach einmaligem Neustart sollte unter „Entwicklerwerkzeuge > Zustände“ der neue Tracker sichtbar sein. Er heißt dann wahrscheinlich binary_sensor.device_tracker_homeoffice
.
Unser neuer kleiner Device-Tracker „Home-Office PCs“ kann nun zum Beispiel auf dem Dashboard verbaut werden. Sobald eines der Geräte eingeschaltet wird, wird auch der Tracker eingeschaltet – beim Ausschalten gibt es knapp zwei Minuten Verzögerung, bis der Home Assistant das Gerät wirklich als offline annimmt.
\nDer Home Assistant kann nun die Programmierung der FRITZ!Box verbessern. Dafür wird in der FRITZ!Box die Heizung zu 100% in den Spar-Modus geschickt, weil ab sofort der Home Assistant die Heizung zwischen Spar- und Komfort-Modus hin- und herschaltet. Die Verbindung ist dabei einfach:
\nUnter „Einstellungen > Automatisierungen & Szenen“ fügen wir dazu ein einfaches Rezept hinzu:
\n# Replace all occurences of `office` with your thermostat's identifier\n# `device_id` has to be assigned via GUI\nalias: "Office: Someone is there"\ndescription: "Be nice, and turn on the heating if someone is using a PC in the office"\ntrigger:\n - platform: state\n entity_id:\n - binary_sensor.device_tracker_homeoffice\n from: "on"\n - platform: state\n entity_id:\n - binary_sensor.device_tracker_homeoffice\n to: "on"\naction:\n - if:\n - condition: state\n entity_id: binary_sensor.device_tracker_homeoffice\n state: "on"\n then:\n - device_id: b46c4851235fb8c90f4a659b6e9a953d\n domain: climate\n entity_id: climate.office\n type: set_preset_mode\n preset_mode: comfort\n else:\n - device_id: b46c4851235fb8c90f4a659b6e9a953d\n domain: climate\n entity_id: climate.office\n type: set_preset_mode\n preset_mode: eco\nmode: single\n
\nErst mit der Zusammenfassung der beiden Zustände zu einem Tracker ist es sauber möglich, zwischen Spar- und Komfort-Modus hin- und herzuschalten. Wenn wir die Tracker trennen würden könnte es ansonsten passieren, dass das Ausschalten eines Rechners die Heizung deaktiviert, obwohl der andere Rechner eigentlich noch läuft.
\nTatsächlich kann man diese Idee auch auf andere Geräte ausdehnen, wie zum Beispiel Fernseher und Spielekonsolen, die ebenfalls im Internet hängen. Eine kleine Automatisierung könnte also abends die Heizung noch etwas länger an lassen, solange der Fernseher läuft.
\nOder wie wäre es mit einem Sammel-Tracker, der die Smartphones aller Haushaltsteilnehmer als Schalter verwendet?
", "link": "https://journal.3960.org/posts/2023-02-14-pragmatische-heizungssteuerung-home-office/", "pubDate": "Tue, 14 Feb 2023 19:04:06 +0100", "atom_published": "2023-02-14T19:04:06+01:00", "atom_updated": "2023-02-21T10:37:45+01:00", "guid": "user/posts/2023-02-14-pragmatische-heizungssteuerung-home-office/index.md", "author": "info@3960.org (Frank Boës)", "categories": [ "Home Assistant", "Programmierung", "Technologie", "The Cool", "AVM", "Homeoffice" ] }, { "title": "Nothing else matters", "description": "\nEinige Lieder sollte man nicht covern… bis jemand vorbei kommt, der einem Klassiker die Krone aufsetzt.
", "content_encoded": "\nEinige Lieder sollte man nicht covern… bis jemand vorbei kommt, der einem Klassiker die Krone aufsetzt.
", "link": "https://journal.3960.org/posts/2023-01-31-nothing-else-matters/", "pubDate": "Tue, 31 Jan 2023 18:40:08 +0100", "atom_published": "2023-01-31T18:40:08+01:00", "atom_updated": "2023-01-31T18:40:08+01:00", "guid": "user/posts/2023-01-31-nothing-else-matters/index.md", "author": "info@3960.org (Frank Boës)", "categories": [ "Musik" ] }, { "title": "JavaScript: Pattern zum Event-Handling", "description": "Manche JavaScript-Applikationen haben eine erhebliche Zahl an Event-Listenern. Diese ordentlich und übersichtlich zu erfassen und abzuarbeiten kann eine Herausforderung sein – bis wir uns die Anleitung zu addEventListener
genau durchlesen.
Manche JavaScript-Applikationen haben eine erhebliche Zahl an Event-Listenern. Diese ordentlich und übersichtlich zu erfassen und abzuarbeiten kann eine Herausforderung sein – bis wir uns die Anleitung zu addEventListener
genau durchlesen.
Wie schon in dem Artikel „Event-Handling mit JavaScript – und ohne jQuery“ angemerkt, ist ein gutes Verständnis von addEventListener
sehr hilfreich, um sich in der eigenen JavaScript-Applikation keine Performance-Ärger einzubrocken – und gleichzeitig die Übersicht zu behalten, wenn die Anzahl der Listener schnell ansteigt.
Gehen wir davon aus, dass ihr klassen- bzw. objektorientiert arbeitet. Schnell werdet ihr auf die Herausforderung treffen, dass this
innerhalb des Listeners eine andere Bedeutung hat als außerhalb.
Natürlich könntet ihr mit .bind()
den Kontext neu setzen. Oder ihr könntet mit anonymen Arrow-Funktionen arbeiten, die aber den üblichen Nachteil von anonymen Funktionen bei Listenern mitbringen.
MDN's Anleitung für addEventListener
verrät uns, dass als Listener drei verschiedene Dinge eingetragen werden können:
null
handleEvent
-Methode verfügt.Die letzte Option erlaubt es uns, this
mit wenig Aufwand vorhersehbar einzusetzen.
Tatsächlich ist die Übergabe des gesamten Objekts als Listener und die Definition einer handleEvent
-Methode überraschend simpel:
// 1st example: Multiple event listeners attached,\n// one handler\nclass App {\n constructor() {\n document.querySelectorAll('a, button').forEach(function(element) {\n element.addEventListener('click', this);\n });\n }\n\n handleEvent(event) {\n // do something\n }\n}\n
\nInnerhalb von handleEvent
kann nun this
mit dem zu erwartenden Bezug auf das aktuelle Klassen-Objekt verwendet werden, während die für das Event notwendigen Eigenschaften (ebenfalls regulär) in dem Parameter event
stehen.
Ärgerlicherweise werden nun aber alle so registrierten Events mit ein und dem selben Listener bedient. Zum Glück verrät uns aber jedes Event, für welches Element es ausgelöst wurde – die sogenannte Event-Delegation. Eine Fallunterscheidung z.B. nach ID kann uns also helfen, die einzelnen Aktionen auseinander zu halten:
\n// 2nd example: Multiple event listeners attached,\n// one handler with delegation per id\nclass App {\n constructor() {\n document.querySelectorAll('a, button').forEach(function(element) {\n element.addEventListener('click', this);\n });\n }\n\n handleEvent(event) {\n switch (event.target.id) {\n case 'example-1':\n // do something\n break;\n case 'example-2':\n // do something other\n break;\n }\n }\n}\n
\n…und aus dem Nachteil ist ein Vorteil geworden, da jetzt die einzelnen Event-Listenern nichts anderes als Fälle in einer switch
-Anweisung geworden sind.
Jetzt können wir die Vielzahl der im DOM verteilten Event-Listener eigentlich auch gleich auf einen einzigen reduzieren, da der Listener sowieso nochmals überprüft, welches Element genau getroffen wurde. Dabei machen wir uns Event-Bubbling zu Nutze, und montieren an zentraler Stelle einen Event-Listener, der alle Interaktionen abfängt, und dann mittels Event-Delegation den korrekten Ansprechpartner auswählt.
\nDie zentralste Stelle ist natürlich… der <body>
. Damit erschlagen wir übrigens gleich den Fall, dass nachträglich dynamisch DOM-Elemente auf der Seite hinzugefügt werden. Denn der auf dem <body>
montierte Listener fängt natürlich auch erst nachträglich im DOM montierte Elemente ab.
// 3rd example: Single event listeners attached,\n// one handler with delegation per id\nclass App {\n constructor() {\n document.body.addEventListener("click", this);\n }\n\n handleEvent(event) {\n switch (event.target.id) {\n case 'example-1':\n // do something\n break;\n case 'example-2':\n // do something\n break;\n }\n }\n}\n
\nNoch weiter abstrahieren lässt sich das Delegations-Beispiel, wenn wir vorher die unterschiedlichen Event-Typen auseinandersortieren:
\n// 4th example: Single event listeners attached,\n// one handler triggers subhandlers per event type\nclass App {\n constructor() {\n document.body.addEventListener("input", this, true);\n document.body.addEventListener("click", this, true);\n document.body.addEventListener("change", this, true);\n }\n\n handleEvent(event) {\n // @see https://developer.mozilla.org/en-US/docs/Web/API/Event\n switch (event.type) {\n case 'click' : this.handleEventClick(event); break;\n case 'input' : this.handleEventInput(event); break;\n case 'change': this.handleEventChange(event); break;\n }\n }\n\n handleEventClick(event) {\n switch (event.target.id) {\n case 'example-1':\n // do something\n break;\n case 'example-2':\n // do something\n break;\n }\n }\n\n handleEventInput(event) {\n switch (event.target.id) {\n case 'example-3':\n // do something\n break;\n case 'example-4':\n // do something\n break;\n }\n }\n\n handleEventChange(event) {\n switch (event.target.id) {\n case 'example-5':\n // do something\n break;\n case 'example-6':\n // do something\n break;\n }\n }\n}\n
\nDieses Pattern lässt sich natürlich auch ohne Objekte einsetzen, und mit regulären Funktionsaufrufen durchführen. Dabei verliert es aber natürlich die Möglichkeit, this
ohne Gehirn- oder Code-Akrobatik zu verwenden.
Unser schönes Beispiel geht davon aus, dass wir die Auswahl der eigentlich durchzuführenden Aktion an der id
des Elements festmachen, dass das Event ausgelöst hat. Tatsächlich können wir das aber an jedes Attribut koppeln. Damit sind auch class
-Attribute mögliche Ziele.
<dialog>\n <button class="modal-close">Close<button>\n \n <label for="date">Input</label>\n <input id="date" type="date" class="change-date" />\n\n <button class="set-to-current">Set to current time</button>\n</dialog>\n
\nDas erlaubt uns zum Beispiel, die erste Klasse eines Elements darüber entscheiden zu lassen, was getan werden soll:
\n// 5th example: Single event listeners attached,\n// one handler triggers subhandlers per `class` attribute\nclass App {\n constructor() {\n document.body.addEventListener("input", this, true);\n document.body.addEventListener("click", this, true);\n document.body.addEventListener("change", this, true);\n }\n\n handleEvent(event) {\n // Get first `class` of element\n const handler = e.target.classList.item(0);\n switch (handler) {\n case 'modal-close' : this.handleModalClose(event); break;\n case 'change-data' : this.handleChangeDate(event); break;\n case 'set-to-current': this.handleSetToCurrent(event); break;\n }\n }\n\n handleModalClose(event) {\n // do something\n }\n\n handleChangeDate(event) {\n // do something\n }\n\n handleSetToCurrent(event) {\n // do something\n }\n}\n
\nAber wie wäre es mit einem eigenen data
-Attribut, zum Beispiel in Form eines data-handler
?
<dialog>\n <button data-handler="modal-close">Close<button>\n \n <label for="date">Input</label>\n <input id="date" type=="date" data-handler="change-date" />\n\n <button data-handler="set-to-current">Set to current time</button>\n</dialog>\n
\nIn diesem Fall hängen wir an alle HTML-Elemente ein data-handle
, mit dem wir zum Beispiel auch direkt benennen können, mit welchem Handler gearbeitet werden soll:
// 5th example: Single event listeners attached,\n// one handler triggers subhandlers per `data-handler` attribute\nclass App {\n constructor() {\n document.body.addEventListener("input", this, true);\n document.body.addEventListener("click", this, true);\n document.body.addEventListener("change", this, true);\n }\n\n handleEvent(event) {\n // Bubble up to closest DOM element with `data-handler`\n const handler = e.target.closest('[data-handler]')?.dataset.handler;\n switch (handler) {\n case 'modal-close' : this.handleModalClose(event); break;\n case 'change-data' : this.handleChangeDate(event); break;\n case 'set-to-current': this.handleSetToCurrent(event); break;\n }\n }\n\n handleModalClose(event) {\n // do something\n }\n\n handleChangeDate(event) {\n // do something\n }\n\n handleSetToCurrent(event) {\n // do something\n }\n}\n
\nAls angenehmer Nebeneffekt können wir uns auf die Elemente beschränken, die auch wirklich ein data-handler
-Attribut haben – oder im DOM nach oben wandern, bis wir ein Element gefunden haben, dass über ein solches Attribut verfügt. \n.closest()
ist hier eine wirkliche Hilfe.
Der Home Assistant hat gegenüber vielen anderen Smart-Home-Lösungen einen Vorteil: Er benötigt keinen Internet-Dienst, um zu funktionieren. Damit ist er von vorne herein sehr sicher, da initial keine Daten aus eurem Smart-Home ins Internet entweichen.
\nEinen kleinen Nachteil hat diese Konstruktion aber: Euer Home Assistant lässt sich nicht außerhalb eures Heimnetzwerks steuern. Oder?
", "content_encoded": "Der Home Assistant hat gegenüber vielen anderen Smart-Home-Lösungen einen Vorteil: Er benötigt keinen Internet-Dienst, um zu funktionieren. Damit ist er von vorne herein sehr sicher, da initial keine Daten aus eurem Smart-Home ins Internet entweichen.
\nEinen kleinen Nachteil hat diese Konstruktion aber: Euer Home Assistant lässt sich nicht außerhalb eures Heimnetzwerks steuern. Oder?
\n\nNatürlich stimmt das nicht: Direkt im Home Assistant wird Werbung für die Home Assistant Cloud gemacht. Mit diesem kleinen Dienst kann eure vormals nicht im Internet erreichbare Lösung auch außerhalb eures Heimnnetzwerks angesprochen werden. Neben der Tatsache, dass ihr damit ein Sicherheitsfeature eures Home Assistants über Bord geworfen habt, kostet dieser Dienst aber auch Geld.
\nTatsächlich ist der entscheidende Satz, dass euer Home Assistant nur aus eurem Heimnetzwerk zugreifbar ist… beziehungsweise, wenn ihr Zugriff auf euer Heimnetzwerk habt. Wenn ihr also mittels eine Virtual Private Networks auch außerhalb eurer WLAN-Reichweite euch in euer Heimnnetz einwählen könnt, könnt ihr natürlich auch auf euren Home Assistant zugreifen.
\nDas schöne an dieser Lösung: Der Zugang zu eurem System ist somit nur durch eine VPN-Verbindung möglich; die Daten eures Systems wie auch der Zugang zum Home Assistant sind nicht direkt im Internet verfügbar.
\nDie AVM FRITZ!Box bietet netterweise einen eingebauten VPN-Dienst, der euch mit beliebigen Betriebssystemen für PCs, Laptops und Smartphones eine VPN-Verbindung erlaubt. Die Einrichtung ist nicht schwierig, aber voller Fallstricke.
\nRichtet also auf eurer AVM FRITZ!Box einen VPN-Zugang ein, aber mit einer wichtigen Änderung: Wenn euer Home Assistant bereits mit der FRITZ!Box eingerichtet wurde, solltet ihr keinesfalls (wie von AVM vorgeschlagen) eure FRITZ!Box (und damit alle anderen Netzwerkteilnehmer) mit neuen IPs versehen. Denn der Home Assistant merkt sich für so ziemlich jedes Gerät explizit die IP, und eine Änderung ist ein größerer Schmerz. Tatsächlich könnt ihr die IP eurer FRITZ!Box so belassen – und verliert nur die Möglichkeit, über einen Router mit der selben IP das VPN zu benutzen. Tatsächlich erhaltet ihr aber bei einer Mobilverbindung immer eine IP außerhalb des Heimbereichs, könnt also problemlos das VPN nutzen.
\nAn eurem Home Assistant muss keine weitere Änderung vorgenommen werden. Sobald ihr die VPN-Verbindung aufgebaut habt, könnt ihr sowohl im Browser als auch über die App den Home Assistant benutzen, als ob ihr im Heimnetzwerk wärt.
\nInteressanterweise kann eure Home Assistant App (zumindest unter Android) auch außerhalb des Heimnetzwerks Benachrichtigungen eures Home Assistants empfangen. Das liegt daran, dass der Home Assistant mit dem Übertragen der Benachrichtigungen einen Android-Dienst außerhalb eures Netzwerks beauftragt, der euch die Nachrichten über das Internet zustellt.
\nHeizungssteuerung ist in aller Munde. Als Besitzer einer FRITZ!Box habe ich mir also ein paar FRITZ!DECT 301 Thermostate der Firma AVM zugelegt, um meine Heizung sparsamer zu machen.
\nUnd dann bin ich dummerweise auf Home Assistant aufmerksam gemacht worden, mit dem man die etwas spartanischen Möglichkeiten der Thermostate gewaltig aufbohren kann.
\nBegleitet mich auf meiner Reise in den Kaninchenbau.
", "content_encoded": "Heizungssteuerung ist in aller Munde. Als Besitzer einer FRITZ!Box habe ich mir also ein paar FRITZ!DECT 301 Thermostate der Firma AVM zugelegt, um meine Heizung sparsamer zu machen.
\nUnd dann bin ich dummerweise auf Home Assistant aufmerksam gemacht worden, mit dem man die etwas spartanischen Möglichkeiten der Thermostate gewaltig aufbohren kann.
\nBegleitet mich auf meiner Reise in den Kaninchenbau.
\n\nBei der Steuerung der Thermostate ist wichtig zu wissen, dass der Home Assistant (wie auch die FRITZ!Box selber) nur in festen Intervallen Aufträge an die Thermostate übertragen kann. Im schlimmsten Fall dauert es bis zu 15 Minuten, bis euer Auftrag dort angekommen ist. Wenn ihr also Automatisierungen oder eine Steuerung bastelt, müsst ihr etwas Vorlauf in Kauf nehmen.
\nMeine Philosophie bei der Integration des Home Assistant war wie folgt:
\nNach der Installation wird man von den etwas überzüchteten Heizkörperreglern in der Oberfläche des Home Assistant begrüßt. Meine Regler sollen nur aus drei Teilen bestehen:
\nDas ist tatsächlich schön konfigurativ zu lösen…
\n\n…oder mit dem folgendem YAML als schnelles Rezept für das Dashboard:
\n# Replace all occurences of `wohnzimmer` with your thermostat's identifier\ntype: thermostat\nfeatures:\n - style: icons\n preset_modes:\n - eco\n - comfort\n type: climate-preset-modes\nentity: climate.wohnzimmer\n
\n(Update 2024–02: Die neuen Konfigurationsmöglichkeiten des Home Assistant haben den Einbau weiterer Knöpfe deutlich vereinfacht.)
\nDurch den Home Assistant werden die FRITZ!DECT 301 Thermostate folgerichtig als Thermostate erkannt. Tatsächlich beinhalten sie aber auch einen Temperatur-Sensor, den man mit etwas Bastelei auch im Dashboard anzeigen kann.
\nMit dem Home Assistant Community Add-on: Visual Studio Code kann die configuration.yaml
um die folgenden Zeilen erweitern werden:
# Converting thermostats into thermometers\n# Replace all occurences of `wohnzimmer` with your thermostat's identifier\ntemplate:\n - sensor:\n - name: "Wohnzimmer Heizung Temperatur"\n unique_id: wohnzimmer_heizung_temperatur\n state: "{{ state_attr('climate.wohnzimmer', 'current_temperature') }}"\n unit_of_measurement: "°C"\n device_class: temperature\n
\nDanach können diese Sensoren im Dashboard angezeigt werden:
\n# Replace all occurences of `wohnzimmer` with your thermostat's identifier\ntype: gauge\nentity: sensor.wohnzimmer_heizung_temperatur\nmin: 10\nmax: 32\nseverity:\n green: 21\n yellow: 23\n red: 25\n
\nDie FRITZ!DECT 301 Thermostate können in einen Urlaubsmodus geschickt werden – ein Zustand, der im Home Assistant ebenfalls ausgelesen werden kann. Besonders luxuriös geschieht das mit einem eigenen Binär-Sensor.
\nMit dem Home Assistant Community Add-on: Visual Studio Code kann die configuration.yaml
um die folgenden Zeilen erweitern werden:
# Replace all occurences of `wohnzimmer` with your thermostat's identifier\ntemplate:\n - binary_sensor:\n - name: "Wohnzimmer Heizung Urlaubsmodus"\n unique_id: wohnzimmer_heizung_urlaubsmodus\n state: "{{ state_attr('climate.wohnzimmer', 'holiday_mode') }}"\n icon: "mdi:bag-checked"\n
\nAnalog lässt sich auch der Sommermodus der Heizung in der configuration.yaml
als eigener Sensor einrichten:
# Replace all occurences of `wohnzimmer` with your thermostat's identifier\ntemplate:\n - binary_sensor:\n - name: "Wohnzimmer Heizung Sommermodus"\n unique_id: wohnzimmer_heizung_sommermodus\n state: "{{ state_attr('climate.wohnzimmer', 'summer_mode') }}"\n icon: "mdi:hvac-off"\n
\nIn der FRITZ!Box hatte ich die Steuerung meiner Büro-Heizung bisher stumpf an die Wochentage Montags bis Freitag gekoppelt. So lief die Heizung auch, wenn ein Feiertag das eigentlich unnötig machte. Höchste Zeit also für den Arbeitstag-Sensor.
\nMit dem Home Assistant Community Add-on: Visual Studio Code kann die configuration.yaml
um die folgenden Zeilen erweitern werden:
# Workday sensor\nbinary_sensor:\n - platform: workday\n country: DE\n province: NI # Niedersachsen, Lower Saxony\n
\nDer Home Assistant kann nun die Programmierung der FRITZ!Box verbessern. Dafür wird in der FRITZ!Box die Heizung zu 100% in den Spar-Modus geschickt, weil ab sofort der Home Assistant die Heizung auf den Komfort-Modus schaltet, wenn ein Arbeitstag vorliegt.
\nVorher legen wir uns einen Zeitplan an, den wir in folgende Automatisierung einbinden:
\n# Replace all occurences of `office` with your thermostat's identifier\n# `device_id` has to be assigned via GUI\nalias: "Home Office"\ndescription: Only trigger comfort mode on work days\ntrigger:\n - platform: state\n entity_id:\n - binary_sensor.office_heizung_home_office\ncondition:\n - condition: and\n conditions:\n - condition: state\n entity_id: binary_sensor.workday_sensor\n state: "on"\n - condition: state\n entity_id: binary_sensor.office_heizung_urlaubsmodus\n state: "off"\naction:\n - if:\n - condition: state\n state: "on"\n entity_id: binary_sensor.office_heizung_home_office\n then:\n - device_id: b46c4851235fb8c90f4a659b6e9a953a\n domain: climate\n entity_id: climate.office\n type: set_preset_mode\n preset_mode: comfort\n else:\n - device_id: b46c4851235fb8c90f4a659b6e9a953a\n domain: climate\n entity_id: climate.office\n type: set_preset_mode\n preset_mode: eco\n enabled: true\nmode: single\n
\nHier können aber auch die Vorlagen in der FRITZ!Box hilfreich sein, um aus dem Home Assistant in der FRITZ!Box komplexere Szenarien auslösen zu können.
", "link": "https://journal.3960.org/posts/2022-11-08-fritz-dect-301-home-assistant/", "pubDate": "Tue, 08 Nov 2022 18:17:23 +0100", "atom_published": "2022-11-08T18:17:23+01:00", "atom_updated": "2024-01-05T12:46:57+01:00", "guid": "user/posts/2022-11-08-fritz-dect-301-home-assistant/index.md", "author": "info@3960.org (Frank Boës)", "categories": [ "Home Assistant", "Programmierung", "Technologie", "The Cool", "AVM", "Homeoffice" ] }, { "title": "Controller testen mit dem JavaScript Gamepad API Test", "description": " Mit dem Gampead API Test könnt ihr die tatsächlichen Rohwerte der Gamepad
API auslesen – und nebenbei eure Gamepads, Joysticks, Schubkontrollen, Fußpedale und sonstige Controller testen.
Mit dem Gampead API Test könnt ihr die tatsächlichen Rohwerte der Gamepad
API auslesen – und nebenbei eure Gamepads, Joysticks, Schubkontrollen, Fußpedale und sonstige Controller testen.
Moderne Browser haben inzwischen eine Vielzahl interessanter JavaScript-Schnittstellen. Höchste Zeit, die Gamepad
-API genau zu begutachten.
Moderne Browser haben inzwischen eine Vielzahl interessanter JavaScript-Schnittstellen. Höchste Zeit, die Gamepad
-API genau zu begutachten.
Die Gamepad
-API ist eine der Zutaten, die moderne Browser für die Spiele-Entwicklung bereit stellen. Neben Grafik (inklusive Virtual Reality), Sound und Socket-Verbindungen gibt es damit alles, was das Spiele-Entwickler-Herz begehrt.
Anleitungen für der Gamepad
-API gibt es viele: Allen voran die Dokumentation zur Gamepad API auf MDN lässt wenig Fragen offen. Ich habe mich aber in einen etwas speziellen Bereich vorgewagt. Inspiriert von einem Retro Flug-Simulator im Browser \nhabe ich mir die Frage gestellt: Wie sieht es aus, wenn ich einen Flugsimulator für den Browser baue, und dort Unterstützung für Joysticks, Schubkontrollen oder Fußpedale integrieren möchte?
Kurz gefasst: Gar nicht so übel.
\nDie Dokumentation zur Gamepad API auf MDN bespricht in den meisten Beispielen nur Gamepads. Der schöne Artikel „Using The Gamepad API In Web Games“ aus dem Smashing Magazine erklärt deutlich mehr die Verwendung von Joysticks & Co – aber auch hier bleiben ein paar Fragen offen.
\nTatsächlich funktioniert die Gamepad API mit so ziemlich jedem USB/Bluetooth-Eingabegerät, das wie ein Gamepad angesprochen werden kann. Darunter fallen Joysticks, Schubkontrollen, Ruder-Pedale und so ziemlich jeder andere HOTAS-Controller, den Flugsimulator-Fans kennen.
\nDer Vorgang ist in JavaScript verblüffend einfach: Mit der JavaScript-Methode Navigator.getGamepads()
erhaltet ihr ein Array aller an eurem PC/Smartphone/sonstigen Endgerät angeschlossenen Controller.
Die Controller verraten folgendes über sich:
\ngamepad.id
: Das ist die ID des Eingabegeräts, die die meisten Browser in einen verständlichen Namen übersetzen – wenn auch jeder Browser ein etwas anderes Ergebnis ausgibt. 😖gamepad.buttons
: Dieses Array verrät euch, wie viele Buttons das Eingabegerät hat.gamepad.axes
: Dieses Array verrät euch, wie viele Achsen das Eingabegerät hat.gamepad.mapping
: Hier verrät der Browser mit dem Schlüsselwort „standard“
, ob der Controller auf ein Standard-Gamepad umbelegt werden konnte.gamepad.timestamp
: Der Zeitpunkt, an dem das letzte Mal eine Messung auf dem Gamepad durchgeführt wurde. Das wird später wichtig, um die Zeit zwischen den Messungen zu verfolgen, und damit z.B. die Dauer eines Tastendrucks zu erkennen.\n
Das Standard-Mapping ist eine große Hilfe für die Implementation von Gamepads in den Browser: Dabei wird die Achsen- und Button-Belegung des Gamepads auf ein erwartbares Layout eines Standard-Gamepads umgebogen. Die Belegung beinhaltet:
\nDer Haken ist, das ein und dasselbe Gamepad nicht von jedem Browser diesem Layout zugeordnet wird. So wird ein Xbox-Controller unter Firefox/Windows, Chrome/Windows und Chrome/Ubuntu als Standard-Controller erkannt, unter Firefox/Ubuntu dagegen nicht. 😖
\nDas kann fatale Auswirkungen haben: Ein und dasselbe Gamepad kann zum Beispiel im Standard-Mapping die analogen Trigger als Buttons mit Analogwerten melden, während der nächste Browser ohne Standard-Mapping die Buttons streicht und stattdessen zwei zusätzliche Achsen sendet (die dann auch noch die Reihenfolge durcheinander bringen). Oder das D-Pad wird nicht als Sammlung von vier Knöpfen sondern als zwei Achsen gemeldet.
\nHöchste Zeit also, einen Blick auf den lokalen Fuhrpark an HOTAS-Controller zu werfen. Mit dem selbstgebauten Gamepad API Test kann jedes beliebige Eingabegerät so dargestellt werden, wie der Browser dieses Gerät auswertet.
\n\n
Ziemlich schnell zeichnet sich dabei ein Muster ab. Jeder Joystick bietet verlässlich folgendes an:
\nBereits dahinter scheiden sich die Geister. In den meisten Fällen könnt ihr euch noch knapp auf die folgende Konvention verlassen:
\nInteressanterweise lassen es sich Joystick-Hersteller nicht nehmen, einige Achsen zu überspringen. Ein vermeintlicher 5-Achsen-Joysticks kann beim Auslesen der API trotzdem neun Achsen senden, wenn der Hersteller des Joysticks das für eine gute Idee hält.
\n\n
Weitere Eingabegeräte wie Schubkontrollen versuchen sich, auch an diese Konvention zu halten. Hier ist aber deutlich mehr Glück gefragt.
\n\n
Ganz verrückt sieht es dann mit Fußpedalen aus, die über gar keine Buttons, aber dafür eine Achse für die Verschiebung der Füße und zwei Achsen für die jeweiligen Pedale haben.
\nDen Zustand des Gamepads und all seiner Achsen und Buttons zu kennen ist für euer Spiel oder eure Simulation nicht immer hilfreich, denn ihr bekommt die absoluten Roh-Daten des Controllers. Diese solltet ihr in der Regel in ein besser handhabbares Format umwandeln.
\n\nDie Programmier-Beispiele in diesem Artikel sind natürlich sehr simpel gehalten; in einer echten Umgebung kann deutlich mehr Abstraktion hilfreich sein.
\n
Analoge Eingabeinstrumente haben in der Regel kleine Ungenauigkeiten. Manchmal zentrieren sie nicht richtig, manchmal flattern die Ausgabewerte etwas. Um diese kleinen Fehler zu korrigieren werden sogenannte dead zones eingerichtet.
\nDead zones oder dead bands ignorieren bestimmte Werte einer Achse, und schneiden diese ab. Somit kann zum Beispiel eine leicht verkalibrierte Mitte eures Sticks trotzdem als 0
ausgelesen werden, oder ein Stick saubere -1
und +1
übermitteln, obwohl er aus Altersschwäche nur +0.9995
erreicht.
Ein fantastischer Artikel zu diesem Thema ist „Joystick input and using deadbands“ von Mimir Games. Einfach gesprochen brauchen wir zwei Arten von Todzonen:
\ninnerThreshold
: Bei selbst zentrierenden Achsen sollte im einer kleinen Zone in der Mitte keine Eingabe genommen werden.outerThreshold
: Generell sollte das obere und untere Ende des Wertebereichs vorzeitig einen Vollausschlag erzeugen.Eine sehr einfache Funktion für diesen Zweck sieht wie folgt aus:
\nconst axisDeadzone = function(axis, outerThreshold, innerThreshold = 0) {\n if (innerThreshold > 0) {\n const multiplier = (axis > 0 ? 1 : -1);\n axis = Math.max(0, (Math.abs(axis) - innerThreshold) / (1 - innerThreshold)) * multiplier;\n }\n\n if (outerThreshold > 0) {\n axis = Math.max(-1, Math.min(1, axis / (1 - outerThreshold)));\n }\n\n return axis;\n}\n
\nDie Gamepad
-API liefert euch immer eine Momentaufnahme des Gamepads, die ihr so oft wie möglich abfragt. Der zeitliche Abstand zwischen diesen Abfragen ist nicht direkt vorhersehbar, sondern wird ebenfalls gemessen. Dafür übermittelt das Gamepad den gamepad.timestamp
.
Den Effekt einer Achs-Eingabe können wir einfach mit der Zeitspanne multiplizieren, der zwischen den letzten beiden Messungen vergangen ist; selbst wenn unser Gamepad etwas länger für die Messung braucht, ist der Effekt im Spiel trotzdem der selbe.
\nZuerst sollten wir uns eine Variable anlegen, in der wir uns den letzten Timestamp merken können. In der Abfrageschleife (dem Game-Loop) können wir nun die Zeit zwischen den letzten beiden Updates ermitteln:
\nlet lastTimestamp = 0;\nlet posX = 0;\nlet posY = 0;\n\nfunction gameLoop () {\n const gamepad = navigator.getGamepads()[0]; // this is just a stub ;)\n\n // Find time elapsed since last gamepad update\n const elapsedTime = lastTimestamp === 0\n ? 0 \n : timestamp - lastTimestamp;\n // Store current timestamp for next time\n lastTimestamp = timestamp;\n\n // Move Pac Man / Thunderblade / Turrican / whatever relative to elapsed time\n posX += gamepad.axes[0] * elapsedTime;\n posY += gamepad.axes[1] * elapsedTime;\n\n window.requestAnimationFrame(gameLoop); // Restart loop\n}\n
\nButtons klingen trivial, sind es aber nicht. Die Gamepad
-API übermittelt, ob ein Button gerade jetzt gedrückt wird. Ob er vorher schon gedrückt wurde und für wie lange, oder ob er in der aktuellen Messung das erste Mal gedrückt wurde – diese Information müssen wir uns selber erarbeiten.
Folgende Zustände sind für uns hilfreich:
\npressed
: Der Button wird gerade gedrückt gehaltentriggered
: Der Button wird gerade zum ersten Mal gedrücktreleased
: Der Button wird gerade wieder losgelassenIn einem Skript könnte das wie folgt aussehen:
\nlet lastButtonStates = [];\n\nfunction gameLoop () {\n const gamepad = navigator.getGamepads()[0]; // this is just a stub ;)\n\n // Get button states\n const buttonStates = gamepad.buttons.map((button, index) => {\n return {\n pressed: button.pressed,\n triggered: lastButtonStates[index]\n ? (button.pressed && lastButtonStates[index].pressed !== button.pressed)\n : button.pressed,\n released: lastButtonStates[index]\n ? (!button.pressed && lastButtonStates[index].pressed !== button.pressed)\n : false\n }\n });\n\n // Have Pac Man / Thunderblade / Turrican / whatever do stuff\n buttonStates[0].triggered && turrican.jump();\n buttonStates[1].pressed && turrican.fire();\n buttonStates[2].pressed && turrican.chargeSuperpower();\n buttonStates[2].released && turrican.releaseSuperpower();\n\n window.requestAnimationFrame(gameLoop); // Restart loop\n}\n
\nMit den Informationen aus triggered
und released
könnt ihr noch die Zeitdauer des Gedrückthaltens ableiten, und zum Beispiel auch in einem bestimmten Intervall bei gedrückter Taste Aktionen ausführen.
Einige unscheinbar Kontrollen an eurem Joystick sind in der Auswertung initial etwas kompliziert zu verstehen. Mit einer kleinen Trickkiste könnt ihr euch aber schnell Übersicht verschaffen.
\nSchubkontrollen sind eine ganz normale Achse wie zum Beispiel ein Joystick, mit dem Unterschied, dass sie sich nicht selber zentrieren. Sie haben den selben Wertebereich wie jede andere Achse auch. Der value
kann zwischen -1
..+1
liegen.
Dieser Wertebereich ist für eine Schubkontrolle in der Regel nicht so hilfreich, aber mittels einer einfachen Funktion auf den Bereich 0
..1
transformierbar:
const axisToThrottle = function(axis) {\n return (axis + 1) / 2;\n}\n
\nEinige Buttons (auch gerne Trigger genannt) haben nicht nur einen festen Auslösepunkt, sondern auch einen analoge Achse verbaut. So sind bei den meisten Gamepads die unteren Schultertasten als analoge Trigger ausgeführt. Dies erlaubt euch als Spieleentwickler genau zu messen, wie weit der Button gedrückt wurde.
\nAuf einem Standard-Gamepad wird dies am GampepadButton
als value
gemeldet:
gamepad.button[4] = {\n pressed: true,\n value: 0.4321\n};\n
\nDer value
liegt dann zwischen 0
..1
.
Verflixt wird es, wenn das Eingabegerät nicht als Standard-Gamepad erkannt wird; denn tatsächlich ist der Trigger gar kein Button, sondern eine Achse:
\ngamepad.axes[3] = -0.1358;\n
\n…wobei der Wert zwischen -1
..1
liegt, womit er einen anderen Wertebereich umfasst als in seiner GampepadButton
-Form.
Eine einfache Transformation macht uns die Achse aber wieder zum Button:
\nconst axisToButton = function(axis) {\n return {\n pressed: (axis > -1),\n value: (axis + 1) / 2,\n touched: (axis > -1)\n };\n}\n
\n\n
Jedes Vier-Wege-Kreuz auf eurem Gamepad, Joystick oder Schubkontrolle kann eine kleine Überraschung für euch bereit halten. Tatsächlich gibt es mehrere Wege, wie sie in der Gamepad
-API auftreten können:
Wenn ein Vier- bzw. Acht-Wege-Kreuz als eine einzige Achse auftritt, hat der Hersteller sich etwas besonderes überlegt: Jeder Wert zwischen -1..+1
steht für Ausrichtung des Coolie hats zwischen 0°..315°. In Ruheposition sendet er dagegen einen unmöglich hohen Wert, den ihr verwerfen solltet.
Der value
auf der Achse beginnen mit -1
auf der 0°-Position. Pro 90°-Drehung erhöht sich der value
um 0.5715
, bis er auf 315° bei +1
endet.
Um aus diesem analogen Wert einen von acht Zuständen zu machen hilft eine kleine Funktion:
\n// 7 0 1\n// 6 ∗ 2\n// 5 4 3\nconst axisToEightWay = function(axis) {\n return (axis > 1) \n ? undefined // centered state\n : Math.round((axis + 1) * 7/2); // 0 means "top", 4 means "bottom"\n}\n
\nDiese acht Zustände können auch in zwei Achsen umgerechnet werden:
\nconst eightWaytoAxes = function(value) {\n if (value === undefined) {\n return [0,0];\n }\n\n let x = value % 4 ? 1 : 0;\n x *= value > 4 ? -1 : 1;\n \n let y = ((value +2) % 4) ? 1 : 0;\n y *= value < 2 || value > 6 ? -1 : 1;\n \n return [x,y];\n}\n
\nWenn euch vier Zustände reichen, braucht es eine etwas andere Formel:
\n// 0\n// 3 + 1\n// 2 \nconst axisToFourWay = function(axis) {\n return (axis > 1) \n ? undefined // centered state\n : Math.round((axis + 1) * 7/4) % 4; // 0 means "top", 2 means "bottom"\n}\n
\nDas Auslesen eines Joysticks hat jede Menge Fallstricke. Aus gutem Grund spekulieren Spielekonsolen und viele Spiele auf das Standard-Gamepad, um sich Ärger beim Mapping zu ersparen.
\nAndersherum stellt sich auch im Browser die Herausforderung, dass wir nicht bei jedem angeschlossenen Controller das genaue Mapping vorher kennen können. So oder so werdet ihr nicht darum herum kommen, dem Spieler einen Konfigurationsdialog anzubieten, mit dem die Zuordnung von Spielfunktion zu Controller-Button/Achse festgelegt werden kann. Bei der Speicherung des Mappings kann die Web Storage API weiterhelfen, damit der Aufwand nur einmalig anfällt.
\nEin Mapping könnte jedenfalls wie folgt aussehen:
\nconst mapping = {\n buttons: {\n radio: 2,\n flapsUp: 4,\n flapsDown: 6,\n trimLeft: 7,\n trimRight: 5\n },\n axes: {\n roll: 0,\n pitch: 1,\n throttle: 3,\n yaw: 4,\n freeLook: 9\n }\n}\n
\nEin Zugriff auf die konfigurierten Achsen ist damit auch deutlich übersichtlicher:
\nlet roll = 0;\nlet pitch = 0;\n\nfunction gameLoop () {\n const gamepad = navigator.getGamepads()[0]; // this is just a stub ;)\n\n // ...\n\n roll += gamepad.axes[mapping.axes.roll] * elapsedTime;\n pitch += gamepad.axes[mapping.axes.pitch] * elapsedTime;\n\n window.requestAnimationFrame(gameLoop); // Restart loop\n}\n
\nWichtig für die Konfiguration von Achsen ist es, die genaue Art der Achse zu kennen:
\nIn den meisten Fällen werdet ihr nur wenige absolute Achsen in einem Spiel benötigen. Beim Mapping ist es aber sinnvoll, für einen über eine absolute Achse einstellbaren Wert auch ein Mapping für relative Achsen anzubieten. Als Beispiel kann die Schubkontrolle dienen (die in einer Simulation ihren Wert 1:1 auf die simulierte Schubkontrolle überträgt), die auch durch einen kleinen Stick gesteuert werden könnte (was in der Simulation die simulierte Schubkontrolle nach oben oder unten schiebt).
\nNicht zuletzt müsst ihr beim Mapping berücksichtigen, dass die Anzahl der vorhandenen Achsen und Knöpfe mit dem Eingabegerät des Nutzers nicht abdeckbar ist. Hier bieten sich sogenannte Modifier an: Ähnlich wie die Shift-, Alt- und Strg-Taste die Bedeutung einer Taste auf der Tastatur verändern kann, kann ein gedrückt gehaltener Button die Bedeutung der Achsen und Buttons eines Controllers verändern.
\nDas Mapping-Objekt für Modifier sowie der Konfigurationsdialog sind natürlich deutlich komplexer, können aber selbst Retro-Controller mit wenigen Achsen mit einer erstaunlichen Vielzahl von Funktionen belegen.
\nGame Controller im Browser mittels der Gamepad
-API anzusprechen ist nicht kompliziert – die Interpretation der Werte und Berücksichtigung der verschiedenen Controller hingegen schon. Ein robustes Handling im JavaScript erspart euch und euren Nutzern unschöne Erlebnisse.
Die einzelnen Skripte aus diesem Artikel gibt es übrigens als kompletten GamepadHelper
zum Download. Diese Bibliothek kann eine gute Grundlage für ein eigenes Mapping sein.
SVGs können nicht nur mit Grafikprogrammen wie Adobe Illustrator oder Inkscape erstellt werden, sondern auch mit einem einfachen Texteditor – oder programmatisch mit jeder Skriptsprache, die man so zur Hand hat. Damit die SVG-Grafik aber in jedem Umfeld funktioniert, sind ein paar Besonderheiten zu beachten:
", "content_encoded": "SVGs können nicht nur mit Grafikprogrammen wie Adobe Illustrator oder Inkscape erstellt werden, sondern auch mit einem einfachen Texteditor – oder programmatisch mit jeder Skriptsprache, die man so zur Hand hat. Damit die SVG-Grafik aber in jedem Umfeld funktioniert, sind ein paar Besonderheiten zu beachten:
\n\n<?xml version="1.0" encoding="UTF-8" standalone="no"?>\n<svg version="1.1" viewBox="0 0 100 100" width="210mm" height="210mm" xmlns="http://www.w3.org/2000/svg">\n <title><!-- Document Title goes here --></title>\n <style>\n <!-- CSS goes here -->\n </style>\n <defs>\n <!-- Optional: Pastable objects -->\n </defs>\n <!-- Here comes the actual SVG -->\n</svg>\n
\nDie Grundstruktur eines SVG-Dokuments kann man sich ähnlich wie die eines HTML-Dokuments vorstellen:
\nZuerst kommen Meta-Angaben. Dazu gehört der Dokumenten-Titel, CSS und gegebenenfalls noch <defs>
, falls ihr Grafikkomponenten mehrfach verwenden wollt.
Erst danach sollte die eigentliche Ausgabe von Grafikelementen beginnen.
\nIn SVG hat das <title>
-Tag mehrere Bedeutungen:
<title>
-Tag HTML verwendet, um den Namen eines Dokuments festzulegen.title
-Attributs. Beim Überfahren mit der Maus wird zum Beispiel eine kleiner Tooltip angezeigt.…funktioniert wie in HTML. Dementsprechend sollte man versuchen, nicht jedem Element einzeln mittels stroke
- und fill
-Attributen Styles zuzuordnen, sondern mit CSS das Layout globaler zu definieren.
Tatsächlich gibt es aber ein paar Besonderheiten bei der Verwendung von CSS in SVGs:
\nRGBA-Farben haben die Fähigkeit, darunterliegende Teile einer Grafik durchscheinen zu lassen. Der Alpha-Kanal bestimmt dabei die Durchlässigkeit bzw. Opazität. Leider werden RGBA-Farben nicht in jedem Fall korrekt ausgegeben. Die meisten Browser können es, dafür kaum ein Grafikprogramm oder Bildbetrachter.
\nStattdessen gibt es verschiedene Formen von Opazität bzw. Transparenz.
\nopacity
macht ein gesamtes Element,fill-opacity
nur die Füllung,stroke-opacity
nur die Umrandung opak bzw. transparent.In CSS sieht das wie folgt aus:
\n.black-a-fifty {\n /* fill: rgba(0,0,0,0.5); --- will not always work */\n fill: #000000;\n fill-opacity: 0.5;\n}\n
\nFans von CSS Custom Properties haben bei SVG ebenfalls kein Glück: Die meisten Grafikprogramme können diese Variablen nicht verarbeiten. Stattdessen gibt es aber einen kleinen Ausweg mit dem CSS-Wert currentColor
:
Der Wert für currentColor
wird mit der in SVG sonst irrelevanten CSS-Eigenschaft color
gesetzt (die im Gegensatz zu der gleichnamigen Eigenschaft in HTML nicht aktiv ELemente einfärbt – dafür ist in SVG ja fill
und stroke
da). Der Aufruf von currentColor
wird dann mit dem zuletzt via color
gesetztem Wert gefüllt – ganz wie bei einer CSS Custom Property.
.black {\n color: black; /* sets currentColor to `black` */\n}\n\n.red {\n color: red; /* sets currentColor `red` */\n}\n\n/* These elements will be colored black or red, depending on class */\n\nrect {\n stroke: currentColor;\n fill: currentColor;\n}\n\ntext {\n fill: currentColor;\n}\n
\nIn SVG kann in Theorie nur einzeiliger Text gesetzt werden, beziehungsweise muss jede neue Zeile neu positioniert werden. Mit einem kleinen Trick kann man aber mehrzeiligen Text erstellen, indem jede Zeile in ein <tspan>
-Element gehüllt und diese relativ unter der vorhergehenden Zeile positioniert wird:
<text>\n <tspan x="0">Line 1</tspan>\n <tspan x="0" dy="1.1em">Line 2</tspan>\n <tspan x="0" dy="1.1em">Line 3</tspan>\n <tspan x="0" dy="1.1em">Line 4</tspan>\n</text>\n
\nWichtig ist hier die Definition des Zeilenabstands – diese muss in jedem dy
-Attribut wiederholt werden.
Wenn ihr Links in eurem SVG verwenden wollt, gibt es zwei Wege, wobei je nach Ausgabemedium nur einer von beiden funktioniert… oder auch gar keiner.
\nDie veraltete, aber noch weit verbreitete Methode sind xlink:href
-Attribute, die die Ziel-URL an einem Element festlegen. Damit diese funktionieren, muss zuerst an dem <svg>
-Knoten ein neuer Namespace deklariert werden:
<svg … xmlns:xlink="http://www.w3.org/1999/xlink">\n
\nSobald dieser Namespace bekannt ist, kann das Attribut verwendet werden.
\nIn SVG2 wird stattdessen das aus HTML bekannte <a>
-Tag mit href
-Attribut verwendet.
Für eine maximale Kompatibilität kann man beide Techniken miteinander kombinieren:
\n<a xlink:href="https://www.example.com" href="https://www.example.com">…</a>\n
\nÜbrigens: Die Links für <defs>
und <use>
werden mit dem selben xlink:href
- beziehungsweise href
-Attribut gebaut.
Im Standard sind SVG-Linien eine scharfkantige Sache. Zum Glück gibt es CSS-Eigenschaften, die sowohl die Enden mit stroke-linecap
als auch die Ecken mit stroke-linejoin
abrunden können.
.rounded-line {\n stroke-linecap: round;\n stroke-linejoin: round;\n}\n
\nWenn euer SVG-Dokument auch in Inkscape bearbeitbar sein soll, könnt ihr mit einem zusätzlichen XML-Namensraum kleine Helferlein im Dokument hinterlassen:
\n<svg … xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape">\n
\nJetzt steht euch die Möglichkeit zur Verfügung, die aus Inkscape bekannten Layer in eurem SVG zu verwenden. Dazu müsst ihr eine beliebige Gruppe nur mit zusätzlichen Attributen ausstatten:
\n<g inkscape:groupmode="layer" inkscape:label="Name your layer">…</g>\n
\nWenn eure SVGs nicht nur in modernsten Browsern, sondern auch in älteren Browsern und Grafikprogrammen funktionieren soll, müssen ein paar Spielregeln eingehalten werden. Im Endeffekt ist nach kurzer Umgewöhnung das Erzeugen wunderschöner SVG-Dokumente nicht viel komplizierter als das Erstellen von HTML-Dokumenten.
", "link": "https://journal.3960.org/posts/2022-05-10-svg-kugelsicher-machen/", "pubDate": "Tue, 10 May 2022 19:06:51 +0200", "atom_published": "2022-05-10T19:06:51+02:00", "atom_updated": "2022-05-11T08:52:28+02:00", "guid": "user/posts/2022-05-10-svg-kugelsicher-machen/index.md", "author": "info@3960.org (Frank Boës)", "categories": [ "SVG", "CSS", "Programmierung", "Webdevelop" ] } ] } }