Mail aus Linux via externem SMTP Server

Wollte Mail von einem Linux VPS schicken aber keinen lokalen MTA installieren (Debian) – mail/mailx aus bsd-mailx und mailutils scheinen das nicht zu können (oder ich bin zu blöd und diverse Dokus die das damit beschreiben auch), s-nail aus dem gleichlautenden Paket kanns aber:

echo "Body" | s-nail -Sv15-compat -Smta=smtp://user:pass@smtp.domain.at:Port -Ssmtp-use-starttls -Ssmtp-auth=login -s "Subject" -r "from@domain.at" -a Attachment.txt "to@domain.at"

User und Passwort müssen URL-Encoded sein.

Home Assistant Energiesammlersensoren

Energy Flow Card Plus und Power Flow Card Plus können nur 2 bzw. 4 Individualverbraucher anzeigen – man hat normalerweise aber eher mehr Verbraucher die Daten anliefern.

Man könnte jetzt natürlich in die Tiefen von Typescript absteigen und die 2 Cards erweitern oder man macht Gruppen von Verbrauchern und stellt diese dann dar.

Ich habe 2 gemacht damit Power und Energy ident bleiben können – bei Interesse kann ich mir bei der Power Flow Card noch 2 dazu darstellen; die Details zum Verbrauch findet man ja auch in ansprechenderer Form im Energydashboard.

Hab in Unterhaltungselektronik (PC, TVs, Konsolen, Router, Server, etc.) und Haushaltsgeräte (Kühlschränke, Tiefkühler, Waschmaschine, Geschirrspüler, etc.) unterschieden. Für jede der Kategorien braucht man zwei sensor-Templates.

Power-Template:

template:
  - sensor:  
    - name: "Power Electronics Power"
      unit_of_measurement: "W"
      state: >
        {% set pc1 = states('sensor.zigbee_pc1_power') | float %}
        {% set pc2 = states('sensor.zigbee_pc2_power') | float %}
        {% set tv = states('sensor.zigbee_tv_power') | float %}
        {{ (pc1 + pc2 + tv) | round(1, default=0) }}  

Energy-Template (wichtig ist state_class: total_increasing sonst passiert in der Card genau nix):

template:
  - sensor:
    - name: "Power Electronics Summation delivered"
      state_class: total_increasing
      unit_of_measurement: kWh
      device_class: energy
      state: >
        {% set pc1 = states('sensor.zigbee_pc1_summation_delivered') | float %}
        {% set pc2 = states('sensor.zigbee_pc2_summation_delivered') | float %}
        {% set tv = states('sensor.zigbee_tv_summation_delivered') | float %}
        {{ (pc1 + pc2 + tv) | float }}  

Home Assistant Fronius Event Sensor

Motivation/Information

Die Fronius HA Integration kann nicht die Events (aka “Service Messages”) auslesen – will man also Inverter von der Cloud befreien verliert man die Informationen über Fehler oder sonstige Events am Inverter (die Fronius/SolarWeb sonst per Mail weiterleitet, inklusive XLSX Zusammenfassung pro Tag).

Lösung: Custom component welche vom Inverter die Events ausliest, filtert (aktueller Tag) und die Anzahl an Events als Sensor zur Verfügung stellt – man muss dann zwar noch immer am Inverter nachschauen gehen (Customer Login notwendig obwohl die API/URL ohne Auth verfügbar ist). Trotzdem kann man so auf Events aufmerksam machen und Automations an die Zahl hängen.

Die URL hinter den Events ist http://meininverter/status/events (für den oberen Bereich im GUI (“Current Messages”) gilt wohl die URL http://meininverter/status/activeEvents?sync=true) – liefert JSON array mit folgenden Inhalten:

Im Inverter GUI sieht der Event so aus:

viewernur die mit “1” scheinen im Web GUI vom Inverter auf mögliche andere Werte: 1,3
prefix+eventIDergibt zusammengefügt die zweite Spalte (“Code”)
sourceIDergibt die erste Spalte (“Source”)
timestampUnix FILETIME wann der Event aufgetreten ist (=vierte Spalte im GUI “Time”)
activeUntilUnix FILETIME fünfte Spalte im GUI (“Active until”)

Der Text in der dritten Spalte kann je nach Sprache über eine JSON query in http://meininverter/app/assets/i18n/StateCodeTranslations/en.json erfragt werden (LG2 wäre GEN24 im obigen Beispiel):

Einrichtung

Im Home Assistant unter homeassistant/custom_components einen Ordner “FroniusEvents” erzeugen und folgende Dateien erzeugen:

__init__.py
"""Fronius event sensor"""
manifest.json
{
  "domain": "FroniusEvents",
  "name": "Fronius event sensor",
  "codeowners": [],
  "dependencies": [],
  "documentation": "https://roman.gallauner.at/home-assistant-fronius-event-sensor/",
  "iot_class": "local_polling",
  "requirements": [],
  "version": "1.0.0"
}
sensor.py
"""Platform for Fronius event sensor."""
from __future__ import annotations

import logging
import json
import requests
import time
from datetime import datetime

import voluptuous as vol

# Import the device class from the component that you want to support
import homeassistant.helpers.config_validation as cv
from homeassistant.const import CONF_HOST
from homeassistant.core import HomeAssistant
from homeassistant.components.sensor import (PLATFORM_SCHEMA,SensorEntity)
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType

_LOGGER = logging.getLogger(__name__)

# Validation of the user's configuration
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
    vol.Required(CONF_HOST): cv.string,
})


def setup_platform(
    hass: HomeAssistant,
    config: ConfigType,
    add_entities: AddEntitiesCallback,
    discovery_info: DiscoveryInfoType | None = None
) -> None:
    _LOGGER.debug("setup_platform with host "+config[CONF_HOST])

    # Add devices
    add_entities([FroniusEventsSensor(config[CONF_HOST])])


class FroniusEventsSensor(SensorEntity):
   """Representation of the Fronius events sensor."""

   _host = ""
   _include = ""
        
   _attr_name = "Fronius events today"

   def __init__(self, host) -> None:
       """Initialize FroniusEventsSensor"""
       _LOGGER.debug("__init__ with host "+host)
       self._host=host 
        
   def update(self) -> None:
       """Fetch new state data for the sensor.
        
       This is the only method that should fetch new data for Home Assistant.
       """

       def getEvents(url) -> int:
          _LOGGER.debug("FroniusEventsSensor::getEvents - fetching data from "+url)
          response=requests.get(url)
          data=response.json()
          _LOGGER.debug("FroniusEventsSensor::getEvents - got "+str(len(data))+" events")

          now=datetime.now()
          timestamp_threshold=int(now.replace(hour=0,minute=0,second=0,microsecond=0).timestamp())
          filtered_data=[item for item in data if item['timestamp'] > timestamp_threshold and item['viewer'] == 1]
          _LOGGER.debug("FroniusEventsSensor::getEvents - filtered events: "+str(len(filtered_data)))
          return int(len(filtered_data))

       events=0

       events += getEvents("http://"+self._host+"/status/events")
       events += getEvents("http://"+self._host+"/status/activeEvents?sync=true")
      
       self._attr_native_value = events
       

Im configuration.yaml noch aktivieren:

sensor:
  - platform: FroniusEvents
    host: meininverter

Home Assistant restarten und schon sollte eine neue Integration mit einer Entity (ID sensor.fronius_events_today) auftauchen. Darauf aufbauend kann man dann Automations bauen (z.B. Notify wenn > 0).

Um das DEBUG Logging zu sehen muss man im configuration.yaml für diese Komponente Level entsprechend anpassen, Beispiel:

logger:
  default: info
  logs:
     custom_components.FroniusEvents.sensor: debug

Default Papiergröße/PaperSize eines Druckers mit Powershell ändern

Stand vor der Herausforderung im Loginscript die Etikettengrößen für alle verbundenen Drucker neu zu setzen damit spätere Überraschungen weil Printservice das gerne verstellt/vergisst hintangehalten werden.

$c=@'
using System;
using System.Runtime.InteropServices;
using System.ComponentModel;
using Microsoft.Win32;

   public class PrinterSettings
   {
      [DllImport("kernel32.dll", EntryPoint = "GetLastError", SetLastError = false, ExactSpelling = true, CallingConvention = CallingConvention.StdCall)]
      private static extern Int32 GetLastError();

      [DllImport("winspool.Drv", EntryPoint = "ClosePrinter", SetLastError = true, ExactSpelling = true, CallingConvention = CallingConvention.StdCall)]
      private static extern bool ClosePrinter(IntPtr hPrinter);

      [DllImport("winspool.Drv", EntryPoint="DocumentPropertiesA", SetLastError=true, ExactSpelling=true, CallingConvention=CallingConvention.StdCall)]
      private static extern int DocumentProperties(IntPtr hwnd, IntPtr hPrinter, [MarshalAs(UnmanagedType.LPStr)] string pDeviceName, IntPtr pDevModeOutput, ref IntPtr pDevModeInput, int fMode);

      [DllImport("winspool.Drv", EntryPoint = "OpenPrinterA", SetLastError = true, CharSet = CharSet.Ansi, ExactSpelling = true, CallingConvention = CallingConvention.StdCall)]
      private static extern bool OpenPrinter([MarshalAs(UnmanagedType.LPStr)] string szPrinter, out IntPtr hPrinter, IntPtr PrinterDefaults);

      [DllImport("winspool.drv", CharSet = CharSet.Ansi, SetLastError = true)]
      private static extern bool SetPrinter(IntPtr hPrinter, int Level, IntPtr pPrinter, int Command);

      [StructLayout(LayoutKind.Sequential)]
      public struct PRINTER_INFO_9
      {
         public IntPtr pDevMode;
      }

      private const short CCDEVICENAME = 32;
      private const short CCFORMNAME = 32;
      [StructLayout(LayoutKind.Sequential)]
      public struct DEVMODE
      {
         [MarshalAs(UnmanagedType.ByValTStr, SizeConst = CCDEVICENAME)]
         public string dmDeviceName;
         public short dmSpecVersion;
         public short dmDriverVersion;
         public short dmSize;
         public short dmDriverExtra;
         public int dmFields;
         public short dmOrientation;
         public short dmPaperSize;
         public short dmPaperLength;
         public short dmPaperWidth;
         public short dmScale;
         public short dmCopies;
         public short dmDefaultSource;
         public short dmPrintQuality;
         public short dmColor;
         public short dmDuplex;
         public short dmYResolution;
         public short dmTTOption;
         public short dmCollate;
         [MarshalAs(UnmanagedType.ByValTStr, SizeConst = CCFORMNAME)]
         public string dmFormName;
         public short dmUnusedPadding;
         public short dmBitsPerPel;
         public int dmPelsWidth;
         public int dmPelsHeight;
         public int dmDisplayFlags;
         public int dmDisplayFrequency;
      }
      private const int PRINTER_ACCESS_USE = 0x8;
      private const int DM_OUT_BUFFER = 0x2;
      private const int DM_MODIFY = 0x8;
      private const int DM_PAPERSIZE = 0x2;

      public static bool ChangePaperSize(string sPrinterName, short nSize)
      {
         DEVMODE oDM;
         IntPtr pNativeDM = IntPtr.Zero;
         int nBytesNeeded = 0;
         IntPtr hPrinter = new System.IntPtr();
         PRINTER_INFO_9 oPrinterInfo;
         IntPtr pNativePrinterInfo = IntPtr.Zero;
         IntPtr pDummy=IntPtr.Zero;

         if (!OpenPrinter(sPrinterName, out hPrinter, IntPtr.Zero))
         {
            Console.WriteLine("OpenPrinter() failed with error "+Marshal.GetLastWin32Error()+", aborting.");
            return false;
         }

         nBytesNeeded=DocumentProperties(IntPtr.Zero,hPrinter,sPrinterName,IntPtr.Zero,ref pDummy,0);
         pNativeDM=Marshal.AllocHGlobal(nBytesNeeded);
         DocumentProperties(IntPtr.Zero,hPrinter,sPrinterName,pNativeDM,ref pDummy,DM_OUT_BUFFER);
         oDM = (DEVMODE)Marshal.PtrToStructure(pNativeDM, typeof(DEVMODE));
         Marshal.FreeHGlobal(pNativeDM);

         oDM.dmPaperSize = nSize;
         oDM.dmFields |= DM_PAPERSIZE;

         pNativeDM = Marshal.AllocHGlobal(Marshal.SizeOf(oDM));
         Marshal.StructureToPtr(oDM, pNativeDM, true);

         DocumentProperties(IntPtr.Zero,hPrinter,sPrinterName,pNativeDM,ref pDummy,DM_MODIFY);

         oPrinterInfo.pDevMode = pNativeDM;
         pNativePrinterInfo = Marshal.AllocHGlobal(Marshal.SizeOf(oPrinterInfo));
         Marshal.StructureToPtr(oPrinterInfo, pNativePrinterInfo, true);
         if (!SetPrinter(hPrinter, 9, pNativePrinterInfo, 0))
         {
            Console.WriteLine("SetPrinter() failed with error "+Marshal.GetLastWin32Error()+", aborting.");
            Cleanup(pNativeDM, pNativePrinterInfo, hPrinter);
            return false;
         }
         Cleanup(pNativeDM, pNativePrinterInfo, hPrinter);
         return true;
      }

      public static void Cleanup(IntPtr pNativeDM,IntPtr pNativePrinterInfo,IntPtr hPrinter)
      {
         try
         {
            Console.WriteLine("Memory and handle cleanup");
            if (hPrinter != IntPtr.Zero) ClosePrinter(hPrinter);
            if (pNativeDM != IntPtr.Zero) Marshal.FreeHGlobal(pNativeDM);
            if (pNativePrinterInfo != IntPtr.Zero) Marshal.FreeHGlobal(pNativePrinterInfo);
         }
         catch (Exception)
         {
         }
      }
   }
'@

Add-Type -TypeDefinition $c -ReferencedAssemblies System.Runtime.InteropServices,System.ComponentModel
Add-Type -AssemblyName System.Drawing

$printer="\\meinprintserver\meinverbundenerdrucker"

$ps=New-Object System.Drawing.Printing.PrinterSettings
$ps.PrinterName=$printer
$size=$ps.PaperSizes|? PaperName -like "*meine Etiketten*"

[PrinterSettings]::ChangePaperSize($printer,$size.RawKind)

Größtenteils von hier geklaut und vereinfacht/bereinigt – mit minimalem Fehlerhandling: https://www.codeproject.com/Articles/6899/Changing-printer-settings-using-C

Raspi als WLAN to Ethernet Bridge

Hatte einen Raspberry 3 übrig und einen Inverter ans LAN zu bringen…und das bitte headless.

Mit Raspberry Pi Imager Raspberry Pi OS Lite (64-bit, Bookworm) MicroSD erzeugen – da kann man WLAN, SSH & Co. schön vorkonfigurieren.

Wenn das Ding dann am WLAN online ist über NetworkManager dieses auf eth0 sharen (nur IPv4)


sudo nmcli con add con-name share-wlan type ethernet ifname eth0 ipv4.method shared ipv6.method ignore

Das macht aus eth0 einen Router/DHCP/DNS Server (mit dnsmasq) mit der Adresse 10.42.0.1/24

Was sich am LAN Interface so tut (wenn man MAC Adresse zwecks Fixierung wissen will)


sudo journalctl -u NetworkManager -f

DHCP Leases findet man in


sudo cat /var/lib/NetworkManager/dnsmasq-eth0.leases

Und statisch kann man sie dann hier definieren:


sudo micro /etc/NetworkManager/dnsmasq-shared.d/static-addresses

1 Host pro Zeile:
dhcp-host=mm:mm:mm:mm:mm:mm,10.42.0.xxx

Quelle: https://fedoramagazine.org/internet-connection-sharing-networkmanager/

SonOff ZBDongle-E auf Router flashen

Am besten alles als root durchführen (oder als Normaluser bis zum pip install und danach das source/flashing als root) da naturgemäß der Normaluser nicht direkt aufs Device schreiben darf.

Python virtual environment erzeugen und aktivieren (activate.fish für Fish Shell), Flasher installieren


python -m venv ./universal-silabs-flasher
source ./universal-silabs-flasher/bin/activate 
pip install universal-silabs-flasher

Firmware runterladen (das .gbl File)

https://github.com/itead/Sonoff_Zigbee_Dongle_Firmware/tree/master/Dongle-E/Router

Dongle anstecken, Devicenamen ermitteln (bei mir /dev/ttyACM0) und flashen (passenden Filenamen einsetzen naturgemäß)


ls /dev/serial/by-id

sudo universal-silabs-flasher --device /dev/ttyACM0 flash --firmware ./Z3RouterUSBDonlge_EZNet6.10.3_V1.0.0.gbl

Abstecken und dem Zigbee Netzwerk joinen 😀

Rückflash auf Coordinator ident – man muss nur –sonoff-reset als Parameter mitgeben.

Quelle: https://github.com/NabuCasa/universal-silabs-flasher

Windows 11 Hands Free/Freisprechtelefonie deaktivieren

Situation: Bluetooth Kopfhörer mit Telefoniefunktion aber ohne Mikrofon.

Problem 1: Registriert sich aber in Windows als Mikrofon und Hands-Free/Freisprechtelefoniegerät.

Problem 2: Webex läuft vollig Amok und Audio ist ungenießbar. Nur Webex. Alle anderen Audioquellen inklusive Teams, Zoom, etc. funktionieren einwandfrei.

Lösung: Hands-Free/Freisprechtelefonie Bluetooth Service beim Kopfhörer Device deaktiveren:

shell:::{A8A91A66-3A7D-4424-8D24-04E180695C7A}

Klassisches Control Panel für Geräte und Drucker geht auf:

Beim gewünschten Gerät (bei mir Sony WH-CH520) “Freisprechtelefonie” (englisch: “Hands-free Telephony”) deaktivieren.

AppArmor und Vivaldi

Weiß nicht ob das ein KDE6 oder Vivaldi oder sonst irgendwo anders gelagertes Problem ist aber ich habe einen ganzen Haufen AppArmor Fehler bekommen wenn ich aus Vivaldi etwas heruntergeladenes starten wollte (wenn das Programm in /etc/apparmor.d/abstractions/open-some-applications enthalten ist) – alle rund um /run/user/meine_uid.

Im audit.log habe ich dann viele Einträge mit “failed name lookup – deleted entry” gefunden und bin darauf gestoßen.

Ist vermutlich völlig falsch und viel zu global aber das mediate_deleted aufs top level profil in /etc/apparmor.d/opt.vivaldi-stable gepfropft und schon hats funktioniert:

profile vivaldi-stable /opt/vivaldi/vivaldi-bin flags=(attach_disconnected,mediate_deleted) {

Yen Zeichen statt Backslash im Browser

Hatte unter Linux/in Vivaldi (vermutlich aber in allen Browsern) Yen Zeichen statt Backslashes auf Webseitenteilen die mit Courier New gearbeitet haben.

Lösung: ~/.config/fontconfig/fonts.conf (oder ~/.fonts.conf oder systemweit in /etc/fonts/local.conf einfügen) – hier Verbiegung auf “Hack Nerd Font Mono”:

<?xml version="1.0"?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">

<fontconfig>
 <!-- Replace Courier with a better-looking font -->
 <match target="pattern" name="family">
   <test name="family" qual="any"><string>Courier New</string></test>
   <edit name="family" mode="assign">
     <string>Hack Nerd Font Mono</string>
   </edit>
 </match>
</fontconfig>

Quellen:
https://wiki.archlinux.org/title/Font_configuration
https://askubuntu.com/questions/28419/how-to-most-elegantly-replace-courier-new-with-another-font-system-wide

WordPress an Fediverse andocken

Wer seinen Blog ans Fediverse andocken will: https://wordpress.org/plugins/activitypub/, aus irgendeinem Grund werden die Simple Permalinks (/?p=nnn) nicht akzeptiert – hab daher auf die Variante mit Postname als URL-Teil umgestellt und damit alle gespeicherten Permalinks vernichten müssen 🙁

Im Fediverse kann man auf alle Fälle jetzt über blog@roman.gallauner.at oder https://roman.gallauner.at/@blog folgen 🙂

Dreist geklaut von: https://vkc.sh/activitypub-activate/