Updating the Powerline adapters in my home network

I have blogged previously about a couple of problems with using Powerline adapters in my home network:

As my NETGEAR XAV1301 (200 Mbps) Powerline adapters bought in 2012 apparently do not fully support IPv6, and as my NETGEAR XAV5221 (500 Mbps) adapters bought in 2016 are no longer manufactured either, I decided to invest in some new Powerline adapters that would guarantee IPv6 support. My Web searches did not confirm that the current models of NETGEAR Powerline adapters support IPv6, so I decided to try TP-Link Powerline adapters because the TP-Link Web site states that all current TP-Link Powerline adapters support IPv6. I wanted Powerline adapters for five devices (router, smart TV and three computers), plus the ability to use a mains plug on at least two of those (i.e. so-called ‘pass-through’ adapters). I also wanted to avoid buying different models, in order to minimise the possibility of any problems. TP-Link have a range of 600 Mbps adapters under the name ‘AV600’, so I plumped for two TP-PL4010 adapters (single Ethernet port per adapter), one TP-PL4010P adapter (single Ethernet port and one mains pass-through socket) and one TP-PL4020P (two Ethernet ports and one mains pass-through socket). These all use the Qualcomm Atheros QCA7420 Powerline chipset (which happens to be the same chipset used in my old NETGEAR XAV5221 500 Mbps adapters).

Like NETGEAR, TP-Link does not have a Powerline utility program for Linux, so I had to install TP-Link’s tpPLC utility program in Windows 10 running in a VM (virtual machine) in order to configure the four TP-Link adapters and set the ‘Powerline network name’ to avoid crosstalk with my neighbour’s Powerline adapters that use the factory default network name (‘HomePlugAV’).

Anyway, I got everything set up and working, but soon noticed that there were quite frequent dropouts of the connection to my router and the Internet. Some dropouts did occur when I was using the old NETGEAR Powerline adapters, but I was surprised to find that the performance of the new TP-Link adapters was much worse. The dropouts typically lasted a minute or two. This was annoying, to say the least.

I started searching the Web, and ‘TP-Link’ and ‘dropout’ occur together a lot. I had already disabled Power Saving Mode in the adapters, so knew that was not the cause. I happen to know someone who also uses TP-Link adapters, and he mentioned that he also experienced frequent dropouts. In addition to turning off Power Saving Mode, he had implemented a shell script on his machines to ping an Internet site periodically to try and keep the connection from dropping out, but this did not appear to make any difference. I wrote the script below to try the same thing, and it did not cure the dropouts either:

#!/bin/bash
#
# Script to try to keep the Powerline adapter connected to this machine
# from dropping the connection to the router
#
FIRSTPASS=1
PREVIOUS=2
while true
do
    ping -W 2 -c 1 8.8.8.8 >>/dev/null 2>&1
    STATUS=$?
    if [ $PREVIOUS -ne 0 ] && [ $STATUS -eq 0 ]; then
        logger "Ping successful: connection to Internet is up."
#        echo "Ping successful: connection to Internet is up."
    elif [ $PREVIOUS -eq 0 ] && [ $STATUS -ne 0 ]; then
        logger "Ping unsuccessful: connection to Internet may be down."
#        echo "Ping unsuccessful: connection to Internet may be down."
    elif [ $FIRSTPASS -eq 1 ] && [ $STATUS -ne 0 ]; then
        logger "Ping unsuccessful: connection to Internet may be down."
#        echo "Ping unsuccessful: connection to Internet may be down."
    fi
    PREVIOUS=$STATUS
    FIRSTPASS=0
    sleep 10
done

In my Web searches I came across a a thread in the TP-Link SOHO Community forums with a URL for a new version of firmware for TP-Link Powerline adapters that use the Qualcomm Atheros QCA7420 chipset. I learned from the TP-Link forums that the firmware in NVM (Non-Volatile Memory) depends on the chipset manufacturer’s chipset, not on the Powerline manufacturer’s adapter model, whereas the adapter’s PIB (Parameter Information Block) does change depending on the model (including the country). So I started searching online for a PIB file for the three models of TP-Link adapter that I am using, but I could not find them. However, the Linux open-plc-tools command ‘plctool‘ enabled me to read the PIB from each adapter and store it as a file:

user $ sudo plctool -i eth0 -p TL-PA4010P.pib <MAC address printed on the adapter>
user $ sudo plctool -i eth0 -p TL-PA4010_TV.pib <MAC address printed on the adapter>
user $ sudo plctool -i eth0 -p TL-PA4010_HOME-HUB.pib <MAC address printed on the adapter>
user $ sudo plctool -i eth0 -p TL-PA4020P.pib <MAC address printed on the adapter>

The Ethernet interface in the computer I used is named ‘eth0′, so change it accordingly. You can give any name to the PIB files.

It is also easy to find out the adapters’ MAC addresses and current firmware by using another open-plc-tools command:

user $ plcstat -t -i eth0

The TP-Link tpPLC utility for Windows also shows the firmware version. I was surprised to see that the firmware version was different in the three models I had just bought:

  • TL-PA4010P firmware version: 1.4.0.20-00_401115_191120_901
  • TL-PA4010 firmware version: 1.3.1.2141-00_401013_171025_901
  • TL-PA4020P firmware version: 1.4.0.20-00_402114_191120_901

The command to update the firmware in an adapter using the NVM file I downloaded from the URL in the above-mentioned TP-Link Community forum thread and the PIB file read from the relevant adapter, is as follows:

user $ sudo plctool -i <interface> -P <PIB file> -N <NVM file> -R <MAC address of adapter>

For example:

user $ sudo plctool -i eth0 -P TL-PA4010P.pib -N FW-QCA7420-1.5.0.0026-02-CS-20200114.nvm -R 15:B3:D2:D8:5F:BA

I am fortunate in that the three models of TP-Link Powerline adapter I bought all use the Qualcomm Atheros QCA7420 chipset, so I could use the same NVM file for all four adapters that I bought. I only needed to repeat the command with a different PIB file for each adapter model. The plcstat command can be used to check that the firmware version is different from the factory original version:

user $ plcstat -t -i eth0

Actually, the tpPLC utility in Windows 10 also has the ability to upload an NVM file and a PIB file to an adapter, so, as I have tpPLC installed in a VM, I can use that instead to update firmware in my TP-Link Powerline adapters.

And what difference did upgrading the firmware in my new TP-Link adapters make? A big difference. There are no more dropouts; the connection is now stable and I no longer get interruptions while browsing the Internet. It’s a pity that TP-Link does not supply every chipset’s latest firmware file and every model’s PIB file on their support Web site so that users can update their Powerline adapters.

A Linux command-line utility to discover and list WSD-enabled computers and printers on a home network

In an earlier post I covered the installation and use of wsdd, a WS-Discovery (WSD) daemon that can run on Linux machines and enable machines running Microsoft Windows 10 to discover Linux machines in File Explorer now that Windows 10 has dropped Computer Browser, NetBIOS and SMBv1. All my Linux machines in my home network have wsdd running alongside NetBIOS broadcast name resolution, SMBv2 (used by my Android phone) and SMBv3 (used by my Linux machines). If any visitors to my house happen to bring a laptop running Windows 10, they will be able to discover my SMB shares in File Explorer, which I have always been able to do in Linux and in earlier Windows releases that supported NetBIOS and Computer Browser.

As I pointed out in a comment to another of my earlier posts, a downside of not using the (insecure) SMBv1 protocol is that the Samba utility smbtree incorrectly returns nothing if you enter the command smbtree when using SMBv2 or SMBv3. As all the Linux machines in my home network are running the wsdd daemon in addition to NetBIOS, SMBv2 and SMBv3 — and any visitors’ laptops could be running Windows 10 — it would be nice to have a command-line utility that would discover all machines. Well, here is a stab at such a utility, written by a close relative of mine as a learning exercise in WSD and Python, and is provided here as-is without any warranty or support. It consists of the following five files:

wsd-discover.sh

#!/bin/bash

function del-tmp-files() {
   if ls /tmp/wsd-*.txt 1> /dev/null 2>&1; then
      rm /tmp/wsd-*
   fi
return 0
}

# Delete pre-existing temporary work files.

del-tmp-files

# Get the V5 UUID of this machine
UUID=$(python3 $HOME/discover/wsd-gen-uuid.py)

# Send a multicast probe to all WSD capable devices and store the XML output in wsd-probe1.txt
echo
echo "Please wait.....sending multicast discovery probe and waiting 2 seconds for responses"
echo
python3 $HOME/discover/wsd-mcast-probe.py > /tmp/wsd-probe1.txt

# Iterate through the XML until the UUID to IPv4 mappings are obtained in wsd-probe9.txt
more /tmp/wsd-probe1.txt | grep Computer | awk -F "<wsa:Address>" '{print $2}' > /tmp/wsd-probe2.txt

sort -u /tmp/wsd-probe2.txt > /tmp/wsd-probe3.txt

more /tmp/wsd-probe3.txt | awk -F "uuid:" '{print $2}' > /tmp/wsd-probe4.txt

more /tmp/wsd-probe4.txt | awk -F "</wsa:Address>" '{print $1,"******",$2}' > /tmp/wsd-probe5.txt

more /tmp/wsd-probe5.txt | awk -F "from" '{print $1,"******",$2}' > /tmp/wsd-probe6.txt

more /tmp/wsd-probe6.txt | awk -F "******" '{print $1 $3}' > /tmp/wsd-probe7.txt

more /tmp/wsd-probe7.txt | awk -F "\\\('" '{print $1 $2}' > /tmp/wsd-probe8.txt

more /tmp/wsd-probe8.txt | awk -F "'" '{print $1}' > /tmp/wsd-probe9.txt

# Read the UUID to IPv4 mappings until end of file and send XML requests to each WSD host

while read RECORD; do

	URN=$(echo $RECORD | cut -d" " -f1)
	IPA=$(echo $RECORD | cut -d" " -f2)

	# Generate the HTTP/XML request file from the template
	cat $HOME/discover/wsd-template.xml | sed 's/XXXXXXXXXX/'$URN'/g' > /tmp/wsd-request.txt
	cat /tmp/wsd-request.txt | sed -i 's/YYYYYYYYYY/'$UUID'/g' /tmp/wsd-request.txt

	# Send the XML/SOAP request to the target machine
	curl -s -A wsd --header "Accept-Encoding: identity" --header "Connection: Close" \
	--header "Content-Type: application/soap+xml" --header "User-Agent: wsd" \
	--data @/tmp/wsd-request.txt http://$IPA:5357/$URN > /tmp/wsd-response-$IPA.txt

	# Extract, format and display the information returned
	echo
	echo "Device IP : $IPA"
	echo "==========================="
	echo -n "Name         :";cat /tmp/wsd-response-$IPA.txt | awk -F "FriendlyName" '{print $2}' | awk -F "<" '{print $1}' | cut -d">" -f2
	echo -n "Manufacturer :";cat /tmp/wsd-response-$IPA.txt | awk -F "Manufacturer" '{print $2}' | awk -F "<" '{print $1}' | cut -d">" -f2
	echo -n "Model        :";cat /tmp/wsd-response-$IPA.txt | awk -F "ModelName" '{print $2}' | awk -F "<" '{print $1}' | cut -d">" -f2
	echo -n "Category     :";cat /tmp/wsd-response-$IPA.txt | awk -F "DeviceCategory" '{print $2}' | awk -F "<" '{print $1}' | cut -d">" -f2
	echo -n "URN          :";cat /tmp/wsd-response-$IPA.txt | awk -F "Address" '{print $2}' | awk -F "<" '{print $1}' | cut -d">" -f2
	echo -n "Type         :";cat /tmp/wsd-response-$IPA.txt | awk -F "Types" '{print $2}' | awk -F "<" '{print $1}' | cut -d">" -f2
	echo -n "Workgroup    :";cat /tmp/wsd-response-$IPA.txt | awk -F "<pub:Computer" '{print $2}' | awk -F "<" '{print $1}' | cut -d">" -f2
	echo

done < /tmp/wsd-probe9.txt

# This next bit is just a bit of fluff to display printers. The formatting is inconsistent because every printer
# has a different web page. Printer manufacturers are listed in the file $HOME/discover/printers.txt. If the printer
# is not in this file it won't be found in the HTTP information

# Check whether the original multicast response contains any printer information
cat /tmp/wsd-probe1.txt | grep -q -A2 Print

if [[ $? -eq 0 ]]; then # A printer of some sort has been found

   # Get the line that contains 'Print' and the two lines after it (one of which contains the printer IP and URL)
   more /tmp/wsd-probe1.txt | grep -A2 Print > /tmp/wsd-probe10.txt

   # Remove any duplicate entries
   sort -u /tmp/wsd-probe10.txt > /tmp/wsd-probe11.txt

   # Isolate the printer IP and URL information
   cat /tmp/wsd-probe11.txt | awk -F"XAddrs>" '{print $2}' | awk -F"/wsd" '{print $1}' > /tmp/wsd-probe12.txt

   # Remove blank lines to clean up the file
   sed '/^$/d' /tmp/wsd-probe12.txt > /tmp/wsd-probe13.txt

   # Read each line of the file containing the printer URLs and contact the printers in turn
   while read RECORD; do

	echo "Printers"
	echo "==========================="
	URL=$RECORD
	# Try to get the printer's HTML page
        curl -s $URL/index.html > /tmp/wsd-printer.txt
	if [[ $? -ne 0 ]]; then
           echo "Couldn't get HTML info from $URL"
	else
	   # Read each line of the printers.txt file and try to get the Make and Model from the HTML
           while read PRT; do
		 grep -q $PRT /tmp/wsd-printer.txt
		 if [[ $? -eq 0 ]]; then # Printer in the list is contained in the returned HTML
		    # Extract the Make and the following word hoping it's the Model
		    TYP=$(grep $PRT /tmp/wsd-printer.txt | awk -v a=$PRT '{for(i=1;i<=NF;i++) if ($i==a) print $i,$(i+1)}')
		    echo "URL   : $URL"
                    echo "Make  : $TYP"
		 fi
	   done < $HOME/discover/printers.txt
	fi

   done < /tmp/wsd-probe13.txt

fi

echo

#
# Delete the latest temporary work files.
#
del-tmp-files

wsd-gen-uuid.py

import uuid
import socket

hostName = (socket.gethostname())

# nameSpaces = [uuid.NAMESPACE_DNS, uuid.NAMESPACE_URL, uuid.NAMESPACE_OID, uuid.NAMESPACE_X500]
nameSpaces = [uuid.NAMESPACE_DNS]

for namespace in nameSpaces:

    print (uuid.uuid5(namespace, hostName))

wsd-mcast-probe.py

import socket
import struct
import sys
import uuid

# Create a V1 UUID for the MessageID based on the host address and current time
# The MessageID must be unique but it isn't necessary to have anything other than a V1 UUID
uuid1 = uuid.uuid1()
myuuid = str(uuid1)
print ("Generating UUID for MessageID")
print(myuuid)

# The string 'message' is a template WSD probe that is multicast to group 239.255.255.250 port 3702
# The template should not change unless there is a major change to the WSD specifications
# Escape double quotation marks within the message string (but not the outer double quotation marks)
message = "<?xml version=\"1.0\" encoding=\"utf-8\"?><soap:Envelope xmlns:pnpx=\"http://schemas.microsoft.com/windows/pnpx/2005/10\" xmlns:pub=\"http://schemas.microsoft.com/windows/pub/2005/07\" xmlns:soap=\"http://www.w3.org/2003/05/soap-envelope\" xmlns:wsa=\"http://schemas.xmlsoap.org/ws/2004/08/addressing\" xmlns:wsd=\"http://schemas.xmlsoap.org/ws/2005/04/discovery\" xmlns:wsdp=\"http://schemas.xmlsoap.org/ws/2006/02/devprof\" xmlns:wsx=\"http://schemas.xmlsoap.org/ws/2004/09/mex\"><soap:Header><wsa:To>urn:schemas-xmlsoap-org:ws:2005:04:discovery</wsa:To><wsa:Action>http://schemas.xmlsoap.org/ws/2005/04/discovery/Probe</wsa:Action><wsa:MessageID>urn:uuid:" + myuuid + "</wsa:MessageID></soap:Header><soap:Body><wsd:Probe><wsd:Types>wsdp:Device</wsd:Types></wsd:Probe></soap:Body></soap:Envelope>"

# Convert the message to a UTF-8 byte string
bytstr = message.encode('utf-8')

# Define a variable for the multicast group and multicast destination port
multicast_group = ('239.255.255.250', 3702)
multicast_address = '239.255.255.250'

# Cheeky way to get the Internet facing Ethernet IP address for use further down
# Create a socket, pretend to use it to connect to an Internet service. Nothing is actually sent
# but the IP address of the Internet facing interface is returned 
def get_ip_address():
    sock1 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    sock1.connect(("8.8.8.8", 80))
    return sock1.getsockname()[0]

# Create datagram socket 1 for multicasts and allow the IP address and port to be reused in case something
# else is using them e.g. the WSD service
sock1 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock1.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock1.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)

IPADDR = (get_ip_address())

# Set the multicasts TTL to 1 so they stay on the local segment
sock1.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 1)

# Set a timeout so the socket stops listening if no data is received within the timeout
# This prevents it from locking up
sock1.settimeout(2.0)

# Bind the socket to the IP and port that we wish to use as the source IP and port of datagrams we transmit
# AND the destination IP and port of datagrams that we receive
sock1.bind ((IPADDR, 3702))

# Join the 239.255.255.250 multicast group. This isn't necessary if this script is being run on a machine
# that is also running the wsdd daemon. Joining the multicast group allows the script to be run on any
# machine regardless

mreq = struct.pack("4sl", socket.inet_aton(multicast_address), socket.INADDR_ANY)
sock1.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq)

try:

    # Send the WSD probe (bytstr) to the multicast group and port
    # print ('\nsending "%s"' % bytstr)
    sent = sock1.sendto(bytstr, multicast_group)

    # Listen for up to 4096 byte responses from all responders to the multicast message
    while True:
       print ('\nwaiting to receive responses')
       try:
           data, addr = sock1.recvfrom(4096)

           # We could use the format below to split 'addr' into its component IP and port fields but is isn't necessary
           # data, (ip, port) = sock1.recvfrom (4096)

       except:
           # This exception only occurs if no data is received on socket for the timeout period
           print ('\ntimed out, no more responses socket1')
           break
       else:
           # This is the response data that the bash script writes out to the wsd-probe1.txt file
           print ('\nreceived %s from %s' % (data.decode('utf-8'), addr))

finally:
    print ('\nsocket closed\n')

wsd-template.xml

<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope
      xmlns:pnpx="http://schemas.microsoft.com/windows/pnpx/2005/10"
      xmlns:pub="http://schemas.microsoft.com/windows/pub/2005/07"
      xmlns:soap="http://www.w3.org/2003/05/soap-envelope"
      xmlns:wsa="http://schemas.xmlsoap.org/ws/2004/08/addressing"
      xmlns:wsd="http://schemas.xmlsoap.org/ws/2005/04/discovery"
      xmlns:wsdp="http://schemas.xmlsoap.org/ws/2006/02/devprof"
      xmlns:wsx="http://schemas.xmlsoap.org/ws/2004/09/mex">
      <soap:Header>
            <wsa:To>urn:uuid:XXXXXXXXXX</wsa:To>
            <wsa:Action>http://schemas.xmlsoap.org/ws/2004/09/transfer/Get</wsa:Action>
            <wsa:MessageID>urn:uuid:fe11d044-bc13-11ea-b98c-2c56dc778d37</wsa:MessageID>
            <wsa:ReplyTo>
                 <wsa:Address>http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</wsa:Address>
            </wsa:ReplyTo>
            <wsa:From>
                 <wsa:Address>urn:uuid:YYYYYYYYYY</wsa:Address>
            </wsa:From>
      </soap:Header>
      <soap:Body />
</soap:Envelope>

printers.txt

Brother
Canon
Epson
HP
Kodak
Lexmark

How to install

user $ mkdir $HOME/discover

Use a text editor to create the five files listed above in the directory $HOME/discover/.

Make the Bash script and the Python scripts executable:

user $ chmod u+x $HOME/discover/*.sh $HOME/discover/*.py

How to use

user $ $HOME/discover/wsd-discover.sh

The script will list discovered devices (see the caveat in the Description section below). For example:

user $ $HOME/discover/wsd-discover.sh

Please wait.....sending multicast discovery probe and waiting 2 seconds for responses


Device IP : 192.168.1.121
===========================
Name         :WSD Device tutankhamun
Manufacturer :wsdd
Model        :wsdd
Category     :Computers
URN          :urn:uuid:ff03f853-8a45-5ad9-b75b-fe4f632c8c5b
Type         :pub:Computer
Workgroup    :TUTANKHAMUN/Workgroup:HOME


Device IP : 192.168.1.10
===========================
Name         :WSD Device akhanaten
Manufacturer :wsdd
Model        :wsdd
Category     :Computers
URN          :urn:uuid:ad8fedfb-a22c-5551-92b4-653aae69f379
Type         :pub:Computer
Workgroup    :AKHANATEN/Workgroup:HOME


Device IP : 192.168.1.74
===========================
Name         :WSD Device thutmoseiii
Manufacturer :wsdd
Model        :wsdd
Category     :Computers
URN          :urn:uuid:9bf49ac3-e58d-57a4-87ea-7c0d5ef02234
Type         :pub:Computer
Workgroup    :THUTMOSEIII/Workgroup:HOME

Printers
===========================
URL   : http://192.168.1.78:80
Make  : Canon MP560


The example output above was for a network of three Linux machines running the wsdd daemon, connected via Ethernet, plus a printer connected via Wi-Fi.

Description

The scripts are non-intrusive and discover WSD-enabled devices in multicast group 239.255.255.250 port 3702, namely a) Windows 10 and b) other Linux machines running the WSD daemon wsdd or other WSD software. It runs over Ethernet and Wi-Fi. The script joins the multicast group (with a reusable socket) and sends out a WSD Probe. The responses contain the UUID-to-IP address mappings of the devices it discovers. Each discovered device is then contacted individually on its IP address TCP port 5357 to retrieve basic information.

If you run the script on Linux with the WSD Daemon (wsdd) also running (see earlier post), the script discovers itself as well as other devices. If you run the script on a machine that is not running the WSD Daemon it still discovers other devices, but not itself.

The script also discovers any WSD-enabled printers that listen for multicasts on UPnP / SSDP group 239.255.255.250 but don’t care about what UDP port is being used. If a WSD-enabled printer is detected, the script attempts to retrieve the make and model of the printer using HTTP. To detect different printer makes, add the manufacturer e.g. Canon, Epson, Lexmark etc. to the file ‘printers.txt‘. The script reports on the printer make and tries to extract the model type. It may not always format the output 100% accurately.

The main thing to bear in mind is that the scripts do not maintain state i.e. a single discovery probe is transmitted. Multicast is fundamentally unreliable and only devices that respond are reported. If the probe is lost or an end device doesn’t respond, for whatever reason, it doesn’t get reported. You can run the script a few times to ensure that it picks up as many of the devices as it possibly can.

Reconfiguring the time zone, locales and keymaps in Sabayon Linux

This is an example of how to reconfigure the time zone, locales and keymaps in a Sabayon Linux installation from the command line. Sabayon Linux uses systemd, therefore much of this example should also be applicable in other Linux distributions that use systemd, and will certainly be applicable in Gentoo Linux installations that use systemd rather than OpenRC.

You can check the currently selected keymaps (console and X Windows) and locale using the ‘localectl status‘ command. For example:

user $ localectl status
   System Locale: LANG=en_GB.UTF-8
       VC Keymap: uk
      X11 Layout: gb
       X11 Model: pc105

Let’s say I had previously configured my installation to use only the en_GB and en_US locales but I now want to add Swiss Italian. The steps would be as shown below. I will assume the system being configured is in Switzerland and therefore I will also reconfigure the time zone accordingly, but that is not essential.

Check if the desired time zone exists:

root # timedatectl list-timezones | grep Zurich
Europe/Zurich

Set the desired time zone:

root # timedatectl set-timezone Europe/Zurich

Check if the Swiss Italian locale (it_CH) has already been added:

root # localectl list-locales
C.utf8
en_GB
en_GB.iso88591
en_GB.utf8
en_US
en_US.iso88591
en_US.utf8

If the desired locale is not present, add it:

root # nano /etc/locale.gen
root # grep -v "^#\|^$" /etc/locale.gen
C.UTF-8 UTF-8
en_GB.UTF-8 UTF-8
en_GB ISO-8859-1
en_US.UTF-8 UTF-8
en_US ISO-8859-1
it_CH.UTF-8 UTF-8
it_CH ISO-8859-1

Generate the locales:

root # locale-gen
 * Generating 7 locales (this might take a while) with 1 jobs
 *  (1/7) Generating C.UTF-8 ...                                          [ ok ]
 *  (2/7) Generating en_GB.ISO-8859-1 ...                                 [ ok ]
 *  (3/7) Generating en_GB.UTF-8 ...                                      [ ok ]
 *  (4/7) Generating en_US.ISO-8859-1 ...                                 [ ok ]
 *  (5/7) Generating en_US.UTF-8 ...                                      [ ok ]
 *  (6/7) Generating it_CH.ISO-8859-1 ...                                 [ ok ]
 *  (7/7) Generating it_CH.UTF-8 ...                                      [ ok ]
 * Generation complete
 * Adding locales to archive ...                                          [ ok ]

Check the locales have been added:

root # localectl list-locales
C.utf8
en_GB
en_GB.iso88591
en_GB.utf8
en_US
en_US.iso88591
en_US.utf8
it_CH
it_CH.iso88591
it_CH.utf8

Set the desired locale:

root # localectl set-locale LANG=it_CH.UTF-8

Check which Italian console keymaps are available:

root # localectl list-keymaps | grep it
it
it-ibm
it2
mac-it

But let’s say I want to use a Swiss German keymap (sg) for the console instead of an Italian keymap. Check if a console keymap for Swiss German exists:

root # localectl list-keymaps | grep sg
sg
sg-latin1
sg-latin1-lk450

By the way, Debian, Ubuntu and its derivatives store console keymaps differently to some distributions, and the command ‘localectl list-keymaps‘ in Debian, Ubuntu and its derivatives will return an error message (the command ‘localectl set-keymap’ will still work though):

user $ localectl list-keymaps
Failed to read list of keymaps: No such file or directory

Set the console keymap to Swiss German:

root # localectl set-keymap sg

Let’s say I want to use a Swiss keymap in X Windows. Check if it exists:

root # localectl list-x11-keymap-layouts | grep sg
root # localectl list-x11-keymap-layouts | grep ch
ch

Set the X Windows keymap to Swiss:

root # localectl set-x11-keymap ch

Update the environment variables and profile:

root # env-update && source /etc/profile
>>> Regenerating /etc/ld.so.cache...

Edit /etc/default/grub and change (or add, if none exists) the console keymap entry in GRUB_CMDLINE_LINUX_DEFAULT to be vconsole.keymap=sg, and also rd.vconsole.keymap=sg (‘rd‘ stands for ‘RAM disk’) because Sabayon Linux uses an initramfs:

root # nano /etc/default/grub

Regenerate grub.cfg:

root # grub-mkconfig -o /boot/grub/grub.cfg
Generazione file di configurazione GRUB...
Trovato sfondo: /boot/grub/default-splash.png
Trovata immagine linux: /boot/kernel-genkernel-x86_64-5.4.0-sabayon
Trovata immagine initrd: /boot/initramfs-genkernel-x86_64-5.4.0-sabayon
fatto

Reboot to check if everything is working:

root # systemctl reboot

Check that the list of locales is as expected:

root # eselect locale list
Available targets for the LANG variable:
  [1]   C
  [2]   C.utf8
  [3]   en_GB
  [4]   en_GB.iso88591
  [5]   en_GB.utf8
  [6]   en_US
  [7]   en_US.iso88591
  [8]   en_US.utf8
  [9]   it_CH
  [10]  it_CH.iso88591
  [11]  it_CH.utf8
  [12]  POSIX
  [13]  it_CH.UTF-8 *
  [ ]   (free form)

Check if the current configuration is as expected:

root # localectl status
   System Locale: LANG=it_CH.UTF-8
       VC Keymap: sg
      X11 Layout: ch

If the Desktop Environment is KDE, check the file ~/.config/plasma-localerc to see if the LANG variable is set to the locale just configured.

root # cat /home/fitzcarraldo/.config/plasma-localerc
[Formats]
LANG=en_GB.UTF-8

If it is not, delete the file:

root # rm /home/fitzcarraldo/.config/plasma-localerc

then logout, login again and re-check the file:

root # cat /home/fitzcarraldo/.config/plasma-localerc
[Formats]
LANG=it_CH.UTF-8

The system should now be ready for use with the new time zone, locale and keymaps.

user $ date
gio 2 lug 2020, 15:55:21, CEST
user $ localectl status
   System Locale: LANG=it_CH.UTF-8
       VC Keymap: sg
      X11 Layout: ch

Using a mixture of locale variables

It is not mandatory for all the locale variables to be for the same locale. For example, suppose I want to use the currency and number formats of one of the other locales I added. That is not so outlandish: I could be a Swiss national whose mother tongue is Swiss Italian, working in the Swiss branch of a British company and I want the currency format and number format on my work computer to be British, but everything else to be Swiss. To achieve this I can additionally do the following:

root # localectl set-locale LC_MONETARY=en_GB.UTF-8
root # localectl set-locale LC_NUMERIC=en_GB.UTF-8
root # env-update && source /etc/profile

Check that the main locale and keymaps remain as they were but that the two locale variables have been changed to the British locale:

root # localectl status
   System Locale: LANG=it_CH.UTF-8
                  LC_NUMERIC=en_GB.UTF-8
                  LC_MONETARY=en_GB.UTF-8
       VC Keymap: sg
      X11 Layout: ch
root # locale
LANG=it_CH.UTF-8
LC_CTYPE="it_CH.UTF-8"
LC_NUMERIC=en_GB.UTF-8
LC_TIME="it_CH.UTF-8"
LC_COLLATE="it_CH.UTF-8"
LC_MONETARY=en_GB.UTF-8
LC_MESSAGES="it_CH.UTF-8"
LC_PAPER="it_CH.UTF-8"
LC_NAME="it_CH.UTF-8"
LC_ADDRESS="it_CH.UTF-8"
LC_TELEPHONE="it_CH.UTF-8"
LC_MEASUREMENT="it_CH.UTF-8"
LC_IDENTIFICATION="it_CH.UTF-8"
LC_ALL=

You can see above that only $LC_NUMERIC and $LC_MONETARY have changed, as I wanted. As I did not change the time zone, the command I used earlier to set the time zone to Europe/Zurich is still in force:

root # date
gio 2 lug 2020, 16:12:18, CEST

By the way, if you try to change one of the variables from en_GB.UTF-8 back to it_CH.UTF-8, the change does not show in the output of the locale command. For example, let’s say you want to change LC_NUMERIC back to it_CH.UTF-8:

root # localectl status
   System Locale: LANG=it_CH.UTF-8
                  LC_NUMERIC=en_GB.UTF-8
                  LC_MONETARY=en_GB.UTF-8
       VC Keymap: sg
      X11 Layout: ch
root # localectl set-locale LC_NUMERIC=it_CH.UTF-8
root # cat /etc/locale.conf   
LANG=it_CH.UTF-8
LC_MONETARY=en_GB.UTF-8
root # cat /etc/env.d/02locale
LANG=it_CH.UTF-8
LC_MONETARY=en_GB.UTF-8
root # localectl status
   System Locale: LANG=it_CH.UTF-8
                  LC_MONETARY=en_GB.UTF-8
       VC Keymap: sg
      X11 Layout: ch
root # locale
LANG=it_CH.UTF-8
LC_CTYPE="it_CH.UTF-8"
LC_NUMERIC=en_GB.UTF-8  <-- Notice it didn't change
LC_TIME="it_CH.UTF-8"
LC_COLLATE="it_CH.UTF-8"
LC_MONETARY=en_GB.UTF-8
LC_MESSAGES="it_CH.UTF-8"
LC_PAPER="it_CH.UTF-8"
LC_NAME="it_CH.UTF-8"
LC_ADDRESS="it_CH.UTF-8"
LC_TELEPHONE="it_CH.UTF-8"
LC_MEASUREMENT="it_CH.UTF-8"
LC_IDENTIFICATION="it_CH.UTF-8"
LC_ALL=
root # env-update && source /etc/profile
>>> Regenerating /etc/ld.so.cache...
root # locale
LANG=it_CH.UTF-8
LC_CTYPE="it_CH.UTF-8"
LC_NUMERIC=en_GB.UTF-8 <-- Notice it still didn't change
LC_TIME="en_GB.UTF-8"
LC_COLLATE="it_CH.UTF-8"
LC_MONETARY=en_GB.UTF-8
LC_MESSAGES="it_CH.UTF-8"
LC_PAPER="it_CH.UTF-8"
LC_NAME="it_CH.UTF-8"
LC_ADDRESS="it_CH.UTF-8"
LC_TELEPHONE="it_CH.UTF-8"
LC_MEASUREMENT="it_CH.UTF-8"
LC_IDENTIFICATION="it_CH.UTF-8"
LC_ALL=

This is one of the reasons I’m not keen on the layer of abstraction added by systemd. The way to get LC_NUMERIC back to it_CH.UTF-8 is to change all the locale variables to en_GB.UTF-8 then back to it_CH.UTF-8:

root # localectl set-locale LANG=en_GB.UTF-8
root # env-update && source /etc/profile
root # localectl set-locale LANG=it_CH.UTF-8
root # env-update && source /etc/profile
root # reboot

After rebooting, the change will have been applied:

root # locale
LANG=it_CH.UTF-8
LC_CTYPE="it_CH.UTF-8"
LC_NUMERIC="it_CH.UTF-8"
LC_TIME="it_CH.UTF-8"
LC_COLLATE="it_CH.UTF-8"
LC_MONETARY="it_CH.UTF-8"
LC_MESSAGES="it_CH.UTF-8"
LC_PAPER="it_CH.UTF-8"
LC_NAME="it_CH.UTF-8"
LC_ADDRESS="it_CH.UTF-8"
LC_TELEPHONE="it_CH.UTF-8"
LC_MEASUREMENT="it_CH.UTF-8"
LC_IDENTIFICATION="it_CH.UTF-8"
LC_ALL=
root # localectl status
   System Locale: LANG=it_CH.UTF-8
       VC Keymap: sh
      X11 Layout: ch

Notice that everything has changed back to it_CH.UTF-8, including $LC_MONETARY, so you’d have to repeat the command ‘localectl set-locale LC_MONETARY=en_GB.UTF-8‘ if you wanted that to still be the British format.

If you use KDE, also check the contents of the file ~/.config/plasma-localerc to make sure it contains the correct locale:

user $ cat ~/.config/plasma-localerc
[Formats]
LANG=it_CH.UTF-8

Optionally you could edit that file to add desired settings. For example:

[Formats]
LANG=it_CH.UTF-8
LC_CTYPE=it_CH.UTF-8
LC_NUMERIC=en_GB.UTF-8
LC_TIME=it_CH.UTF-8
LC_COLLATE=it_CH.UTF-8
LC_MONETARY=en_GB.UTF-8
LC_MESSAGES=it_CH.UTF-8
LC_PAPER=it_CH.UTF-8
LC_NAME=it_CH.UTF-8
LC_ADDRESS=it_CH.UTF-8
LC_TELEPHONE=it_CH.UTF-8
LC_MEASUREMENT=it_CH.UTF-8
LC_IDENTIFICATION=it_CH.UTF-8
useDetailed=true

Alternatively, delete that file then logout and login again to make KDE Plasma pick up the values of the variables from the existing configuration. KDE Plasma will recreate the file.